Merge branch 'master' into fix-edit-handlers-test

Merge. Remove deprecated tests.

Conflicts:
	wagtail/tests/fixtures/test.json
This commit is contained in:
Tom Talbot 2014-07-01 16:34:43 +01:00
commit bf3e51f6fb
130 changed files with 3969 additions and 918 deletions

View file

@ -4,6 +4,7 @@ Changelog
0.4 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py
* Added notification preferences
* Added 'original' as a resizing rule supported by the 'image' tag
* Hallo.js updated to version 1.0.4
* Snippets are now ordered alphabetically
@ -16,9 +17,14 @@ Changelog
* 'image' tag now accepts extra keyword arguments to be output as attributes on the img tag
* Added an 'attrs' property to image rendition objects to output src, width, height and alt attributes all in one go
* Added 'construct_whitelister_element_rules' hook for customising the HTML whitelist used when saving rich text fields
* Added 'in_menu' and 'not_in_menu' methods to PageQuerySet
* Added 'get_next_siblings' and 'get_prev_siblings' to Page
* Added init_new_page signal
* Fix: Animated GIFs are now coalesced before resizing
* Fix: Wand backend clones images before modifying them
* Fix: Admin breadcrumb now positioned correctly on mobile
* Fix: Page chooser breadcrumb now updates the chooser modal instead of linking to Explorer
* Fix: Embeds - Fixed crash when no HTML field is sent back from the embed provider
0.3.1 (03.06.2014)
~~~~~~~~~~~~~~~~~~

View file

@ -29,6 +29,7 @@ Contributors
* Tom Talbot
* Jeffrey Hearn
* Robert Clark
* Tim Heap
Translators
===========

View file

@ -1,9 +1,15 @@
For Django developers
=====================
.. contents:: Contents
:local:
.. note::
This documentation is currently being written.
Overview
~~~~~~~~
Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the ``Page``, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail's ``Page`` as a base.
Wagtail organizes content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn't prescribe ways to organize and interrelate your content, but here we've sketched out some strategies for organizing your models.
@ -203,7 +209,6 @@ Methods:
* get_context
* get_template
* is_navigable
* get_other_siblings
* get_ancestors
* get_descendants
* get_siblings
@ -269,6 +274,7 @@ not_type(self, model):
return self.get_query_set().not_type(model)
.. _wagtail_site_admin:
Site
~~~~
@ -278,3 +284,13 @@ Django's built-in admin interface provides the way to map a "site" (hostname or
Access this by going to ``/django-admin/`` and then "Home Wagtailcore Sites." To try out a development site, add a single site with the hostname ``localhost`` at port ``8000`` and map it to one of the pieces of content you have created.
Wagtail's developers plan to move the site settings into the Wagtail admin interface.
.. _redirects:
Redirects
~~~~~~~~~
Wagtail provides a simple interface for creating arbitrary redirects to and from any URL.
.. image:: ../images/screen_wagtail_redirects.png

View file

@ -1,7 +1,8 @@
For Front End developers
========================
.. contents::
.. contents:: Contents
:local:
========================
Overview
@ -90,6 +91,9 @@ In addition to Django's standard tags and filters, Wagtail provides some of its
Images (tag)
~~~~~~~~~~~~
.. versionchanged:: 0.4
The 'image_tags' tags library was renamed to 'wagtailimages_tags'
The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
The syntax for the tag is thus::
@ -100,7 +104,7 @@ For example:
.. code-block:: django
{% load image %}
{% load wagtailimages_tags %}
...
{% image self.photo width-400 %}
@ -205,7 +209,7 @@ No validation is performed on attributes add in this way by the developer. It's
Wagtail can assign the image data to another object using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.src }}" width="{{ tmp_photo.width }}"
@ -227,13 +231,16 @@ You can also use the ``attrs`` property as a shorthand to output the ``src``, ``
Rich text (filter)
~~~~~~~~~~~~~~~~~~
.. versionchanged:: 0.4
The 'rich_text' tags library was renamed to 'wagtailcore_tags'
This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly it also expands internal shorthand references to embedded images and links made in the Wagtail editor into fully-baked HTML ready for display.
Only fields using ``RichTextField`` need this applied in the template.
.. code-block:: django
{% load rich_text %}
{% load wagtailcore_tags %}
...
{{ self.body|richtext }}
@ -269,6 +276,9 @@ Wagtail embeds and images are included at their full width, which may overflow t
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~
.. versionchanged:: 0.4
The 'pageurl' tags library was renamed to 'wagtailcore_tags'
pageurl
--------
@ -276,7 +286,7 @@ Takes a Page object and returns a relative URL (``/foo/bar/``) if within the sam
.. code-block:: django
{% load pageurl %}
{% load wagtailcore_tags %}
...
<a href="{% pageurl self.blog_page %}">
@ -287,7 +297,7 @@ Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for
.. code-block:: django
{% load slugurl %}
{% load wagtailcore_tags %}
...
<a href="{% slugurl self.your_slug %}">

View file

@ -1,5 +1,5 @@
Editing API
Defining models with the Editing API
===========
.. note::
@ -22,18 +22,30 @@ Defining Panels
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
There are three types of panels:
There are four basic types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
``InlinePanel( base_model, relation_name, panels=None, classname=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
``FieldRowPanel( children, classname=None)``
This panel is purely aesthetic. It creates a columnar layout in the editing interface, where each of the child Panels appears alongside each others rather than below. Use of FieldRowPanel particularly helps reduce the "snow-blindness" effect of seeing so many fields on the page, for complex models. It also improves the perceived association between fields of a similar nature. For example if you created a model representing an "Event" which had a starting date and ending date, it would be intuitive to find the start and end date on the same "row".
FieldRowPanel should be used in combination with ``col*`` classnames added to each of the child Panels of the FieldRowPanel. The Wagtail editing interface is layed out using a grid system, in which the maximum width of the editor is 12 columns wide. Classes ``col1``-``col12`` can be applied to each child of a FieldRowPanel. The class ``col3`` will ensure that field appears 3 columns wide or a quarter the width. ``col4`` would cause the field to be 4 columns wide, or a third the width.
**(In addition to these four, there are also Chooser Panels, detailed below.)**
Wagtail provides a tabbed interface to help organize panels. Three such tabs are provided:
* ``content_panels`` is the main tab, used for the bulk of your model's fields.
* ``promote_panels`` is suggested for organizing fields regarding the promotion of the page around the site and the internet e.g A field to dictate whether the page should show in site-wide menus, descriptive text that should appear in site search results, SEO friendly titles, OpenGraph meta tag content and other machine-readable information.
* ``settings_panels`` is essentially for non-copy fields. By default it contains the page's scheduled publishing fields. Other suggested fields e.g: a field to switch between one layout/style and another.
Let's look at an example of a panel definition:
@ -55,7 +67,10 @@ Let's look at an example of a panel definition:
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
FieldRowPanel([
FieldPanel('start_date', classname="col3"),
FieldPanel('end_date', classname="col3"),
]),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
@ -254,6 +269,12 @@ Titles
Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding.
Col*
------
Fields within a ``FieldRowPanel`` can have their width dictated in terms of the number of columns it should span. The ``FieldRowPanel`` is always considered to be 12 columns wide regardless of browser size or the nesting of ``FieldRowPanel`` in any other type of panel. Specify a number of columns thus: ``col3``, ``col4``, ``col6`` etc (up to 12). The resulting width with be *relative* to the full width of the ``FieldRowPanel``.
Required Fields
---------------
@ -346,9 +367,9 @@ The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRela
For another example of using model clusters, see :ref:`tagging`
For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_ ).
For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_.
.. _the django-modelcluster github page: https://github.com/torchbox/django-modelcluster
.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster
.. _extending_wysiwyg:

View file

@ -1,3 +1,6 @@
.. _inserting_videos:
Inserting videos into body content
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -9,4 +12,4 @@ As well as inserting videos into a carousel, Wagtail's rich text fields allow yo
.. image:: ../../images/screen21_video_in_editor.png
* A placeholder with the name of the video and a screenshot will be inserted into the text area. Clicking the X in the top corner will remove the video.
* A placeholder with the name of the video and a screenshot will be inserted into the text area. Clicking the X in the top corner will remove the video.

View file

@ -1,3 +1,6 @@
.. _form_builder:
Form builder
============

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -9,6 +9,7 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
:maxdepth: 3
gettingstarted
settings
building_your_site/index
editing_api
snippets

View file

@ -0,0 +1,59 @@
Management commands
===================
publish_scheduled_pages
-----------------------
**./manage.py publish_scheduled_pages**
This command publishes/unpublishes pages that have had these actions scheduled by an editor.
It is recommended to run this command once an hour.
fixtree
-------
**./manage.py fixtree**
This command scans for errors in your database and attempts to fix any issues it finds.
move_pages
----------
**./manage.py move_pages from to**
This mass moves a bunch of pages from one section of the tree to another.
Options:
- **from**
This is id of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
- **to**
This is the id of the page to move pages to.
update_index
------------
**./manage.py update_index**
This command rebuilds the search index from scratch. It is only required when using ElasticSearch.
It is recommended to run this command once a week and at the following times:
- Whenever any pages have been created through a script (eg, import)
- Whenever any changes have been made to models or search configuration
While this command is running, the search may not return any results so avoid running this command at peak times.
search_garbage_collect
----------------------
**./manage.py search_garbage_collect**
Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and sometimes you may want to clean out old search queries.
This command cleans out all search query logs that are more than one week old.

603
docs/settings.rst Normal file
View file

@ -0,0 +1,603 @@
==============================
Configuring Django for Wagtail
==============================
To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see `Writing your first Django app`_. Your project directory will look like the following::
myproject/
myproject/
__init__.py
settings.py
urls.py
wsgi.py
myapp/
__init__.py
models.py
tests.py
admin.py
views.py
manage.py
From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and urlconfs to ``urls.py``. For a more complete view of what's defined in these files, see `Django Settings`_ and `Django URL Dispatcher`_.
.. _Writing your first Django app: https://docs.djangoproject.com/en/dev/intro/tutorial01/
.. _Django Settings: https://docs.djangoproject.com/en/dev/topics/settings/
.. _Django URL Dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/
What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`.
Middleware (settings.py)
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
)
Wagtail requires several common Django middleware modules to work and cover basic security. Wagtail provides its own middleware to cover these tasks:
``SiteMiddleware``
Wagtail routes pre-defined hosts to pages within the Wagtail tree using this middleware. For configuring sites, see :ref:`wagtail_site_admin`.
``RedirectMiddleware``
Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.
Apps (settings.py)
~~~~~~~~~~~~~~~~~~
.. code-block:: python
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'compressor',
'taggit',
'modelcluster',
'django.contrib.admin',
'wagtail.wagtailcore',
'wagtail.wagtailadmin',
'wagtail.wagtaildocs',
'wagtail.wagtailsnippets',
'wagtail.wagtailusers',
'wagtail.wagtailimages',
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'myapp', # your own app
)
Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here ``myapp``) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.
Third-Party Apps
----------------
``south``
Used for database migrations. See `South Documentation`_.
.. _South Documentation: http://south.readthedocs.org/en/latest/
``compressor``
Static asset combiner and minifier for Django. Compressor also enables for the use of preprocessors. See `Compressor Documentation`_.
.. _Compressor Documentation: http://django-compressor.readthedocs.org/en/latest/
``taggit``
Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See :ref:`tagging` for a Wagtail model recipe or the `Taggit Documentation`_.
.. _Taggit Documentation: http://django-taggit.readthedocs.org/en/latest/index.html
``modelcluster``
Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see :ref:`inline_panels` or `the django-modelcluster github project page`_.
.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster
``django.contrib.admin``
The Django admin module. While Wagtail will eventually provide a sites-editing interface, the Django admin is included for now to provide that functionality.
Wagtail Apps
------------
``wagtailcore``
The core functionality of Wagtail, such as the ``Page`` class, the Wagtail tree, and model fields.
``wagtailadmin``
The administration interface for Wagtail, including page edit handlers.
``wagtaildocs``
The Wagtail document content type.
``wagtailsnippets``
Editing interface for non-Page models and objects. See :ref:`Snippets`.
``wagtailusers``
User editing interface.
``wagtailimages``
The Wagtail image content type.
``wagtailembeds``
Module governing oEmbed and Embedly content in Wagtail rich text fields. See :ref:`inserting_videos`.
``wagtailsearch``
Search framework for Page content. See :ref:`search`.
``wagtailredirects``
Admin interface for creating arbitrary redirects on your site. See :ref:`redirects`.
``wagtailforms``
Models for creating forms on your pages and viewing submissions. See :ref:`form_builder`.
Settings Variables (settings.py)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Authentication
--------------
.. code-block:: python
LOGIN_URL = 'wagtailadmin_login'
LOGIN_REDIRECT_URL = 'wagtailadmin_home'
These settings variables set the Django authentication system to redirect to the Wagtail admin login. If you plan to use the Django authentication module to log in non-privileged users, you should set these variables to your own login views. See `Django User Authentication`_.
.. _Django User Authentication: https://docs.djangoproject.com/en/dev/topics/auth/
Site Name
---------
.. code-block:: python
WAGTAIL_SITE_NAME = 'Stark Industries Skunkworks'
This is the human-readable name of your Wagtail install which welcomes users upon login to the Wagtail admin.
Search
------
.. code-block:: python
# Override the search results template for wagtailsearch
WAGTAILSEARCH_RESULTS_TEMPLATE = 'myapp/search_results.html'
WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX = 'myapp/includes/search_listing.html'
# Replace the search backend
WAGTAILSEARCH_BACKENDS = {
'default': {
'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
'INDEX': 'myapp'
}
}
The search settings customize the search results templates as well as choosing a custom backend for search. For a full explanation, see :ref:`search`.
Embeds
------
Wagtail uses the oEmbed standard with a large but not comprehensive number of "providers" (youtube, vimeo, etc.). You can also use a different embed backend by providing an Embedly key or replacing the embed backend by writing your own embed finder function.
.. code-block:: python
WAGTAILEMBEDS_EMBED_FINDER = 'myapp.embeds.my_embed_finder_function'
Use a custom embed finder function, which takes a URL and returns a dict with metadata and embeddable HTML. Refer to the ``wagtail.wagtailemebds.embeds`` module source for more information and examples.
.. code-block:: python
# not a working key, get your own!
EMBEDLY_KEY = '253e433d59dc4d2xa266e9e0de0cb830'
Providing an API key for the Embedly service will use that as a embed backend, with a more extensive list of providers, as well as analytics and other features. For more information, see `Embedly`_.
.. _Embedly: http://embed.ly/
To use Embedly, you must also install their python module:
.. code-block:: bash
$ pip install embedly
Images
------
.. code-block:: python
WAGTAILIMAGES_IMAGE_MODEL = 'myapp.MyImage'
This setting lets you provide your own image model for use in Wagtail, which might extend the built-in ``AbstractImage`` class or replace it entirely.
Email Notifications
-------------------
.. code-block:: python
WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. If omitted, Django will fall back to using the ``DEFAULT_FROM_EMAIL`` variable if set, and ``webmaster@localhost`` if not.
Other Django Settings Used by Wagtail
-------------------------------------
.. code-block:: python
ALLOWED_HOSTS
APPEND_SLASH
AUTH_USER_MODEL
BASE_URL
CACHES
DEFAULT_FROM_EMAIL
INSTALLED_APPS
MEDIA_ROOT
SESSION_COOKIE_DOMAIN
SESSION_COOKIE_NAME
SESSION_COOKIE_PATH
STATIC_URL
TEMPLATE_CONTEXT_PROCESSORS
USE_I18N
For information on what these settings do, see `Django Settings`_.
.. _Django Settings: https://docs.djangoproject.com/en/dev/ref/settings/
Search Signal Handlers
----------------------
.. code-block:: python
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
wagtailsearch_register_signal_handlers()
This loads Wagtail's search signal handlers, which need to be loaded very early in the Django life cycle. While not technically a urlconf, this is a convenient place to load them. Calling this function registers signal handlers to watch for when indexed models get saved or deleted. This allows wagtailsearch to update ElasticSearch automatically.
URL Patterns
------------
.. code-block:: python
from django.contrib import admin
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
admin.autodiscover()
urlpatterns = patterns('',
url(r'^django-admin/', include(admin.site.urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# Optional urlconf for including your own vanilla Django urls/views
url(r'', include('myapp.urls')),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
url(r'', include(wagtail_urls)),
)
This block of code for your project's ``urls.py`` does a few things:
* Load the vanilla Django admin interface to ``/django-admin/``
* Load the Wagtail admin and its various apps
* Dispatch any vanilla Django apps you're using other than Wagtail which require their own urlconfs (this is optional, since Wagtail might be all you need)
* Lets Wagtail handle any further URL dispatching.
That's not everything you might want to include in your project's urlconf, but it's what's necessary for Wagtail to flourish.
.. _complete_example_config:
Ready to Use Example Config Files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These two files should reside in your project directory (``myproject/myproject/``).
settings.py
-----------
.. code-block:: python
import os
PROJECT_ROOT = os.path.join(os.path.dirname(__file__), '..', '..')
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
MANAGERS = ADMINS
# Default to dummy email backend. Configure dev/production/local backend
# as per https://docs.djangoproject.com/en/dev/topics/email/#email-backends
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'myprojectdb',
'USER': 'postgres',
'PASSWORD': '',
'HOST': '', # Set to empty string for localhost.
'PORT': '', # Set to empty string for default.
'CONN_MAX_AGE': 600, # number of seconds database connections should persist for
}
}
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = []
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/London'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-gb'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
# Note that with this set to True, Wagtail will fall back on using numeric dates
# in date fields, as opposed to 'friendly' dates like "24 Sep 2013", because
# Python's strptime doesn't support localised month names: https://code.djangoproject.com/ticket/13339
USE_L10N = False
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = '/media/'
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'change-me'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
)
from django.conf import global_settings
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
)
ROOT_URLCONF = 'myproject.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'wagtaildemo.wsgi.application'
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'compressor',
'taggit',
'modelcluster',
'django.contrib.admin',
'wagtail.wagtailcore',
'wagtail.wagtailadmin',
'wagtail.wagtaildocs',
'wagtail.wagtailsnippets',
'wagtail.wagtailusers',
'wagtail.wagtailimages',
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'myapp',
)
EMAIL_SUBJECT_PREFIX = '[Wagtail] '
INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')
# django-compressor settings
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
)
# Auth settings
LOGIN_URL = 'wagtailadmin_login'
LOGIN_REDIRECT_URL = 'wagtailadmin_home'
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
# WAGTAIL SETTINGS
# This is the human-readable name of your Wagtail install
# which welcomes users upon login to the Wagtail admin.
WAGTAIL_SITE_NAME = 'My Project'
# Override the search results template for wagtailsearch
# WAGTAILSEARCH_RESULTS_TEMPLATE = 'myapp/search_results.html'
# WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX = 'myapp/includes/search_listing.html'
# Replace the search backend
#WAGTAILSEARCH_BACKENDS = {
# 'default': {
# 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
# 'INDEX': 'myapp'
# }
#}
# Wagtail email notifications from address
# WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
# If you want to use Embedly for embeds, supply a key
# (this key doesn't work, get your own!)
# EMBEDLY_KEY = '253e433d59dc4d2xa266e9e0de0cb830'
urls.py
-------
.. code-block:: python
from django.conf.urls import patterns, include, url
from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from django.contrib import admin
from django.conf import settings
import os.path
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
admin.autodiscover()
# Signal handlers
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
wagtailsearch_register_signal_handlers()
urlpatterns = patterns('',
url(r'^django-admin/', include(admin.site.urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
url(r'', include(wagtail_urls)),
)
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
urlpatterns += patterns('',
(r'^favicon\.ico$', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
)

View file

@ -1,3 +1,6 @@
.. _search:
Search
======

View file

@ -42,9 +42,10 @@
<li class="color-teal">color-teal</li>
<li class="color-teal-darker">color-teal-darker</li>
<li class="color-teal-dark">color-teal-dark</li>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
<ul>
<li class="color-salmon">color-salmon</li>
<li class="color-salmon-light">color-salmon-light</li>
</ul>
<ul>
<li class="color-grey-1">color-grey-1</li>
@ -54,6 +55,12 @@
<li class="color-grey-4">color-grey-4</li>
<li class="color-grey-5">color-grey-5</li>
</ul>
<ul>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
</section>
<section id="typography">
@ -149,29 +156,37 @@
<section id="buttons">
<h2>Buttons</h2>
<div href="" class="button">button</div>
<a href="#" class="button">button</a>
<div href="" class="button button-secondary">button-secondary</div>
<a href="#" class="button button-secondary">button-secondary</a>
<div href="" class="button yes">yes</div>
<a href="#" class="button yes">yes</a>
<div href="" class="button no">no / serious</div>
<a href="#" class="button no">no / serious</a>
<div href="" class="button bicolor icon icon-plus">bicolor with icon</div>
<a href="#" class="button bicolor icon icon-plus">bicolor with icon</a>
<div href="" class="button button-small">button-small</div>
<a href="#" class="button button-small">button-small</a>
<div href="" class="button bicolor button-small icon icon-plus">bicolo button-small</div>
<a href="#" class="button bicolor button-small icon icon-plus">bicolo button-small</a>
<div href="" class="button button-secondary no">mixed 1</div>
<a href="#" class="button button-secondary no">mixed 1</a>
<div href="" class="button no bicolor icon icon-cog">mixed 2</div>
<a href="#" class="button no bicolor icon icon-cog">mixed 2</a>
<div class="button no bicolor icon icon-cog">button on a div</div>
<p>Buttons must have interaction possible (i.e be an input or button element) to get a suitable hover cursor</p>
<button>button</button>
<button class="button-small">small button</button>
<button class="bicolor icon icon-plus">bicolor with icon</button>
<input type="submit" class="bicolor icon icon-plus" value="bicolor only supported on button elements" />
<button class="icon icon-view">button</button>
</section>
<section id="dropdowns">
@ -191,7 +206,7 @@
<div class="col3">
<div class="dropdown dropdown-button">
<div class="button">drop down</div>
<input type="button" value="drop down" class="button" />
<div class="dropdown-toggle icon icon-arrow-down"></div>
<ul role="menu">
<li><a href="#">Items in this list do not match button width</a></li>
@ -222,6 +237,24 @@
</div>
</div>
</div>
<div class="row">
<br />
<div class="col3">
<div class="dropdown dropdown-button match-width">
<a href="#" class="button" value="drop down">Link button</a>
<div class="dropdown-toggle icon icon-arrow-down"></div>
<ul role="menu">
<li><a href="#">items should not exceed button width</a></li>
<li><a href="#">item 2</a></li>
</ul>
</div>
</div>
<div class="col3">
<button>button for comparison of height</button>
</div>
</div>
</section>
<section id="header">
@ -381,6 +414,7 @@
<li class="icon icon-warning">warning</li>
<li class="icon icon-success">success</li>
<li class="icon icon-date">date</li>
<li class="icon icon-time">time</li>
<li class="icon icon-form">form</li>
</ul>

View file

@ -23,6 +23,7 @@ class ExampleForm(forms.Form):
url = forms.URLField(required=True)
email = forms.EmailField(max_length=254)
date = forms.DateField()
time = forms.TimeField()
select = forms.ChoiceField(choices=CHOICES)
boolean = forms.BooleanField(required=False)

View file

@ -39,7 +39,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Events",
"numchild": 3,
"numchild": 4,
"show_in_menus": true,
"live": true,
"depth": 3,
@ -183,6 +183,37 @@
"pk": 8,
"model": "tests.formpage",
"fields": {
"to_address": "to@email.com",
"from_address": "from@email.com",
"subject": "The subject"
}
},
{
"pk": 9,
"model": "wagtailcore.page",
"fields": {
"title": "Ameristralia Day",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 4,
"content_type": ["tests", "eventpage"],
"path": "0001000100010004",
"url_path": "/home/events/final-event/",
"slug": "final-event",
"owner": 3
}
},
{
"pk": 9,
"model": "tests.eventpage",
"fields": {
"date_from": "2015-04-22",
"audience": "public",
"location": "Ameristralia",
"body": "<p>come celebrate the independence of Ameristralia</p>",
"cost": "Free"
}
},
@ -265,6 +296,16 @@
]
}
},
{
"pk": 6,
"model": "auth.group",
"fields": {
"name": "Admin non-editors",
"permissions": [
["access_admin", "wagtailadmin", "admin"]
]
}
},
{
"pk": 1,
"model": "wagtailcore.grouppagepermission",
@ -400,6 +441,24 @@
"email": "siteeditor@example.com"
}
},
{
"pk": 6,
"model": "auth.user",
"fields": {
"username": "admin_only_user",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"groups": [
["Admin non-editors"]
],
"user_permissions": [],
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
"email": "admin_only_user@example.com"
}
},
{
"pk": 1,
@ -425,6 +484,24 @@
"fields": {
"text": "test_advert",
"url": "http://www.example.com"
}
},
{
"pk": 1,
"model": "wagtaildocs.Document",
"fields": {
"title": "test document",
"created_at": "2014-01-01T12:00:00.000Z"
}
},
{
"pk": 1,
"model": "wagtailimages.Image",
"fields": {
"title": "test image",
"created_at": "2014-01-01T12:00:00.000Z",
"width": 0,
"height": 0
}
}
]

View file

@ -1,4 +1,5 @@
{% load pageurl %}
{% load wagtailcore_tags %}
<!DOCTYPE HTML>
<html>
<head>

View file

@ -1,4 +1,5 @@
{% load pageurl %}
{% load wagtailcore_tags %}
<!DOCTYPE HTML>
<html>
<head>

View file

@ -1,4 +1,5 @@
{% load pageurl %}
{% load wagtailcore_tags %}
<!DOCTYPE HTML>
<html>
<head>

View file

@ -1,4 +1,4 @@
{% load pageurl %}
{% load wagtailcore_tags %}
<ul>
{% for event in events %}

View file

@ -1,4 +1,5 @@
{% load pageurl %}
{% load wagtailcore_tags %}
<!DOCTYPE HTML>
<html>
<head>

View file

@ -140,6 +140,10 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
def set_page_edit_handler(page_class, handlers):
page_class.handlers = handlers
class EditHandler(object):
"""
Abstract class providing sensible default behaviours for objects implementing
@ -183,19 +187,21 @@ class EditHandler(object):
heading = ""
help_text = ""
def object_classnames(self):
def classes(self):
"""
Additional classnames to add to the <li class="object"> when rendering this
within an ObjectList
Additional CSS classnames to add to whatever kind of object this is at output.
Subclasses of EditHandler should override this, invoking super(B, self).classes() to
append more classes specific to the situation.
"""
return ""
def field_classnames(self):
"""
Additional classnames to add to the <li> when rendering this within a
<ul class="fields">
"""
return ""
classes = []
try:
classes.append(self.classname)
except AttributeError:
pass
return classes
def field_type(self):
"""
@ -261,12 +267,6 @@ class BaseCompositeEditHandler(EditHandler):
"""
_widget_overrides = None
def object_classnames(self):
try:
return "multi-field " + self.classname
except (AttributeError, TypeError):
return "multi-field"
@classmethod
def widget_overrides(cls):
if cls._widget_overrides is None:
@ -326,18 +326,33 @@ class BaseObjectList(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/object_list.html"
def ObjectList(children, heading=""):
def ObjectList(children, heading="", classname=""):
return type('_ObjectList', (BaseObjectList,), {
'children': children,
'heading': heading,
'classname': classname
})
class BaseFieldRowPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/field_row_panel.html"
def FieldRowPanel(children, classname=""):
return type('_FieldRowPanel', (BaseFieldRowPanel,), {
'children': children,
'classname': classname,
})
class BaseMultiFieldPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/multi_field_panel.html"
def classes(self):
classes = super(BaseMultiFieldPanel, self).classes()
classes.append("multi-field")
return classes
def MultiFieldPanel(children, heading="", classname=None):
def MultiFieldPanel(children, heading="", classname=""):
return type('_MultiFieldPanel', (BaseMultiFieldPanel,), {
'children': children,
'heading': heading,
@ -353,25 +368,23 @@ class BaseFieldPanel(EditHandler):
self.heading = self.bound_field.label
self.help_text = self.bound_field.help_text
def object_classnames(self):
try:
return "single-field " + self.classname
except (AttributeError, TypeError):
return "single-field"
def classes(self):
classes = super(BaseFieldPanel, self).classes();
if self.bound_field.field.required:
classes.append("required")
if self.bound_field.errors:
classes.append("error")
classes.append(self.field_type())
classes.append("single-field")
return classes
def field_type(self):
return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
def field_classnames(self):
classname = self.field_type()
if self.bound_field.field.required:
classname += " required"
if self.bound_field.errors:
classname += " error"
return classname
object_template = "wagtailadmin/edit_handlers/field_panel_object.html"
object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
def render_as_object(self):
return mark_safe(render_to_string(self.object_template, {
@ -401,7 +414,7 @@ class BaseFieldPanel(EditHandler):
return [self.field_name]
def FieldPanel(field_name, classname=None):
def FieldPanel(field_name, classname=""):
return type('_FieldPanel', (BaseFieldPanel,), {
'field_name': field_name,
'classname': classname,
@ -597,10 +610,23 @@ def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
})
# This allows users to include the publishing panel in their own per-model override
# without having to write these fields out by hand, potentially losing 'classname'
# and therefore the associated styling of the publishing panel
def PublishingPanel():
return MultiFieldPanel([
FieldRowPanel([
FieldPanel('go_live_at'),
FieldPanel('expire_at'),
], classname="label-above"),
], ugettext_lazy('Scheduled publishing'), classname="publishing")
# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
Page.content_panels = [
FieldPanel('title', classname="full title"),
]
Page.promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
@ -609,3 +635,7 @@ Page.promote_panels = [
FieldPanel('search_description'),
], ugettext_lazy('Common page configuration')),
]
Page.settings_panels = [
PublishingPanel()
]

View file

@ -0,0 +1,4 @@
from django.dispatch import Signal
init_new_page = Signal(providing_args=['page', 'parent'])

View file

@ -1,97 +0,0 @@
(function(jQuery) {
return jQuery.widget('IKS.halloToolbarFixed', {
toolbar: null,
options: {
parentElement: 'body',
editable: null,
toolbar: null,
affix: true,
affixTopOffset: 2
},
_create: function() {
var el, widthToAdd,
_this = this;
this.toolbar = this.options.toolbar;
this.toolbar.show();
jQuery(this.options.parentElement).append(this.toolbar);
this._bindEvents();
jQuery(window).resize(function(event) {
return _this.setPosition();
});
jQuery(window).scroll(function(event) {
return _this.setPosition();
});
if (this.options.parentElement === 'body') {
el = jQuery(this.element);
widthToAdd = parseFloat(el.css('padding-left'));
widthToAdd += parseFloat(el.css('padding-right'));
widthToAdd += parseFloat(el.css('border-left-width'));
widthToAdd += parseFloat(el.css('border-right-width'));
widthToAdd += (parseFloat(el.css('outline-width'))) * 2;
widthToAdd += (parseFloat(el.css('outline-offset'))) * 2;
return jQuery(this.toolbar).css("width", el.width() + widthToAdd);
}
},
_getPosition: function(event, selection) {
var offset, position, width;
if (!event) {
return;
}
width = parseFloat(this.element.css('outline-width'));
offset = width + parseFloat(this.element.css('outline-offset'));
return position = {
top: this.element.offset().top - this.toolbar.outerHeight() - offset,
left: this.element.offset().left - offset
};
},
_getCaretPosition: function(range) {
var newRange, position, tmpSpan;
tmpSpan = jQuery("<span/>");
newRange = rangy.createRange();
newRange.setStart(range.endContainer, range.endOffset);
newRange.insertNode(tmpSpan.get(0));
position = {
top: tmpSpan.offset().top,
left: tmpSpan.offset().left
};
tmpSpan.remove();
return position;
},
setPosition: function() {
var elementBottom, elementTop, height, offset, scrollTop, topOffset;
if (this.options.parentElement !== 'body') {
return;
}
this.toolbar.css('top', this.element.offset().top - this.toolbar.outerHeight());
if (this.options.affix) {
this.toolbar.removeClass('affixed');
scrollTop = jQuery(window).scrollTop();
offset = this.element.offset();
height = this.element.height();
topOffset = this.options.affixTopOffset;
elementTop = offset.top - (this.toolbar.height() + this.options.affixTopOffset);
elementBottom = (height - topOffset) + (offset.top - this.toolbar.height());
if (scrollTop > elementTop && scrollTop < elementBottom) {
this.toolbar.addClass('affixed');
this.toolbar.css('top', this.options.affixTopOffset);
}
} else {
}
return this.toolbar;
},
_updatePosition: function(position) {},
_bindEvents: function() {
var _this = this;
this.element.on('halloactivated', function(event, data) {
_this.setPosition();
return _this.toolbar.show();
});
return this.element.on('hallodeactivated', function(event, data) {
return _this.toolbar.hide();
});
}
});
})(jQuery);

View file

@ -1,11 +1,14 @@
"use strict";
var halloPlugins = {
'halloformat': {},
'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]},
'hallolists': {},
'hallohr': {},
'halloreundo': {},
'hallowagtaillink': {},
'hallowagtaillink': {}
};
function registerHalloPlugin(name, opts) {
halloPlugins[name] = (opts || {});
}
@ -30,7 +33,7 @@ function makeRichTextEditable(id) {
richText.hallo({
toolbar: 'halloToolbarFixed',
toolbarcssClass: 'testy',
toolbarCssClass: (input.closest('.object').hasClass('full')) ? 'full' : '',
plugins: halloPlugins
}).bind('hallomodified', function(event, data) {
input.val(data.content);
@ -57,6 +60,7 @@ function initDateChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
i18n: {
lang: window.dateTimePickerTranslations
@ -66,6 +70,7 @@ function initDateChooser(id) {
} else {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
});
}
@ -75,6 +80,7 @@ function initTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
datepicker: false,
scrollInput:false,
format: 'H:i',
i18n: {
lang: window.dateTimePickerTranslations
@ -93,6 +99,7 @@ function initDateTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i',
scrollInput:false,
i18n: {
lang: window.dateTimePickerTranslations
},
@ -197,7 +204,7 @@ function InlinePanel(opts) {
self.updateMoveButtonDisabledStates = function() {
if (opts.canOrder) {
forms = self.formsUl.children('li:visible');
var forms = self.formsUl.children('li:visible');
forms.each(function(i) {
$('ul.controls .inline-child-move-up', this).toggleClass('disabled', i === 0).toggleClass('enabled', i !== 0);
$('ul.controls .inline-child-move-down', this).toggleClass('disabled', i === forms.length - 1).toggleClass('enabled', i != forms.length - 1);
@ -331,36 +338,65 @@ $(function() {
/* Set up behaviour of preview button */
$('.action-preview').click(function(e) {
e.preventDefault();
var $this = $(this);
var previewWindow = window.open($(this).data('placeholder'), $(this).data('windowname'));
$.ajax({
type: "POST",
url: $(this).data('action'),
data: $('#page-edit-form').serialize(),
success: function(data, textStatus, request) {
if (request.getResponseHeader('X-Wagtail-Preview') == 'ok') {
previewWindow.document.open();
previewWindow.document.write(data);
previewWindow.document.close();
} else {
previewWindow.close();
document.open();
document.write(data);
document.close();
}
},
error: function(xhr, textStatus, errorThrown) {
/* If an error occurs, display it in the preview window so that
we aren't just showing the spinner forever. We preserve the original
error output rather than giving a 'friendly' error message so that
developers can debug template errors. (On a production site, we'd
typically be serving a friendly custom 500 page anyhow.) */
previewWindow.document.open();
previewWindow.document.write(xhr.responseText);
previewWindow.document.close();
var previewWindow = window.open($this.data('placeholder'), $this.data('windowname'));
if(/MSIE/.test(navigator.userAgent)){
submitPreview.call($this, false);
} else {
previewWindow.onload = function(){
submitPreview.call($this, true);
}
});
}
function submitPreview(enhanced){
$.ajax({
type: "POST",
url: $this.data('action'),
data: $('#page-edit-form').serialize(),
success: function(data, textStatus, request) {
if (request.getResponseHeader('X-Wagtail-Preview') == 'ok') {
var pdoc = previewWindow.document;
if(enhanced){
var frame = pdoc.getElementById('preview-frame');
frame = frame.contentWindow || frame.contentDocument.document || frame.contentDocument;
frame.document.open();
frame.document.write(data);
frame.document.close();
var hideTimeout = setTimeout(function(){
pdoc.getElementById('loading-spinner-wrapper').className += 'remove';
clearTimeout(hideTimeout);
}) // just enough to give effect without adding discernible slowness
} else {
pdoc.open();
pdoc.write(data);
pdoc.close()
}
} else {
previewWindow.close();
document.open();
document.write(data);
document.close();
}
},
error: function(xhr, textStatus, errorThrown) {
/* If an error occurs, display it in the preview window so that
we aren't just showing the spinner forever. We preserve the original
error output rather than giving a 'friendly' error message so that
developers can debug template errors. (On a production site, we'd
typically be serving a friendly custom 500 page anyhow.) */
previewWindow.document.open();
previewWindow.document.write(xhr.responseText);
previewWindow.document.close();
}
});
}
});
});

View file

@ -2829,6 +2829,7 @@
} else {
}
return this.toolbar.css('left', this.element.offset().left - 2);
},
_updatePosition: function(position) {},

View file

@ -246,7 +246,7 @@
.xdsoft_calendar td.xdsoft_default,
.xdsoft_calendar td.xdsoft_current,
.xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current{
background: $color-orange;
background: $color-salmon;
color:#fff;
font-weight: 700;
}

View file

@ -2,29 +2,19 @@
position:relative;
@include clearfix();
.dropdown-toggle{
color:white;
text-transform:uppercase;
background-color:$color-teal;
line-height:3em;
padding-left:1em;
padding-right:1em;
cursor:pointer;
&:before,
&:after{
margin:0;
}
}
input[type=button], input[type=submit], button, .button{
padding:1em 0;
input[type=submit], input[type=reset], input[type=button], button, .button{
padding:0;
display:block;
width:100%;
height:3em;
line-height:3em;
text-align:left;
padding-left:1em;
float:left;
}
input[type=submit], input[type=reset], input[type=button], button{
line-height:inherit;
}
ul{
@include unlist();
@ -41,6 +31,7 @@
border-color: rgba(255,255,255,0.2);
border-style: solid;
border-width:1px 0 0 0;
overflow:hidden;
}
a{
@ -115,25 +106,26 @@
li{
border-width:0 0 1px 0;
}
}
}
.button{
float:left;
&:hover{
background-color:$color-teal-darker;
}
}
& > .button + .dropdown-toggle{
.dropdown-toggle{
color:white;
text-transform:uppercase;
background-color:$color-teal;
line-height:2.8em;
cursor:pointer;
height:100%;
border-left:1px solid rgba(255,255,255,0.2);
position:absolute;
right:0;
height:100%;
padding:0 0.5em;
text-align:center;
&:before,
&:after{
margin:0;
}
&:before{
width:1em;
font-size:1.2rem;
@ -143,7 +135,8 @@
background-color:$color-teal-darker;
}
}
&.open > .button + .dropdown-toggle{
&.open .dropdown-toggle{
background-color:$color-teal-darker;
}
@ -193,55 +186,6 @@
}
}
h2 .dropdown{
display:inline-block;
font-size:0.7em;
margin-right:0.5em;
vertical-align:middle;
.dropdown-toggle{
padding:0.5em 0;
border-right:1px solid $color-grey-3;
/* icon */
&:before{
opacity:0.5;
padding:0.2em;
height:1.1em;
text-align:left;
}
&:hover{
background-color:$color-teal;
border-color:transparent;
}
}
&.open{
.dropdown-toggle{
background-color:$color-teal;
}
ul{
left:auto;
}
}
.dropdown-toggle:hover{
background-color:$color-grey-3;
&:before{
color:white;
opacity:1;
}
}
&.open .dropdown-toggle:before{
background-color:$color-teal;
color:white;
opacity:1;
}
}
/* Transitions */
.dropdown ul{
@include transition(opacity 0.2s linear);

View file

@ -1,3 +1,9 @@
/*
These are the generic stylings for forms of any type.
If you're styling something specific to the page editing interface,
it probably ought to go in layouts/page-editor.scss
*/
form {
ul, li{
list-style-type:none;
@ -6,9 +12,6 @@ form {
margin:0;
padding:0;
}
li{
@include row();
}
}
fieldset{
@ -55,6 +58,7 @@ input, textarea, select, .richtext, .tagit{
border: 0 !important;
margin-top:-3px !important;
margin-bottom: -2px !important;
line-height:1em !important;
}
&:hover{
@ -76,13 +80,14 @@ input, textarea, select, .richtext, .tagit{
outline:none;
}
&:after{
/* Add select arrow back on browsers where native ui has been removed */
select ~ span:after{
@include border-radius(0 6px 6px 0);
z-index:0;
position:absolute;
right:1px;
right:0px;
top:1px;
height:95%;
bottom:0px;
width:1.5em;
font-family:wagtail;
content:"q";
@ -94,12 +99,14 @@ input, textarea, select, .richtext, .tagit{
pointer-events:none;
color:$color-grey-3;
background-color:$color-fieldset-hover;
margin:0px 1px 0 0;
margin:0px 1px 1px 0;
.ie &{
display:none;
}
}
.ie &:after{
display:none;
}
}
/* radio and check boxes */
@ -166,34 +173,39 @@ input[type=checkbox]:checked:before{
color:$color-teal;
}
/* Core button style */
/* Core button style
Note that these styles include methods to render buttons the same x-browser, described here:
http://cbjdigital.com/blog/2010/08/bulletproof_css_input_button_heights */
input[type=submit], input[type=reset], input[type=button], .button, button{
font-family:Open Sans,Arial,sans-serif;
@include border-radius(3px);
width:auto;
padding:0.7em 1em;
height:2.4em;
line-height:2.4em;
padding:0 1em;
font-size:0.9em;
font-weight:normal;
vertical-align: middle;
display:inline-block;
background-color: $color-button;
border:1px solid $color-button;
color:white;
text-decoration:none;
text-transform:uppercase;
font-size:0.9em;
white-space: nowrap;
position:relative;
-webkit-font-smoothing: auto;
vertical-align: middle;
line-height:1em;
display:inline-block;
overflow:hidden;
position:relative;
font-weight:normal;
outline:none;
box-sizing:border-box;
-webkit-font-smoothing: auto;
-moz-appearance: none;
-moz-box-sizing:border-box;
&.button-small{
padding:0.55em 0.8em;
font-size:1em;
padding:0 0.8em;
height:2em;
line-height:2em;
font-size:0.85em;
}
&.button-secondary{
@ -261,7 +273,6 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
&.button-small.bicolor{
padding: 0.65em 0.8em;
padding-left:3.5em;
&:before{
@ -280,6 +291,18 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
}
/* Special styles to counteract Firefox's completely unwarranted assumptions about button styles */
input[type=submit], input[type=reset], input[type=button], button{
padding:0 1em;
height: 2.4em;
line-height:inherit;
&.button-small{
height:2em;
line-height:inherit;
}
}
button.icon{
&:before,
&:after{
@ -295,6 +318,7 @@ button.icon{
overflow:hidden;
> li{
@include row();
position:relative;
background-color:white;
padding:1em 10em 1em 1.5em; /* 10em padding leaves room for controls */
@ -394,7 +418,7 @@ li.focused > .help{
opacity:1;
}
.required label:after{
.required .field > label:after{
content:"*";
color:$color-red;
font-weight:bold;
@ -415,58 +439,112 @@ li.focused > .help{
.boolean_field .help, .radio .help{
opacity:1;
}
.iconfield {
position:relative;
/*
This is expected to go on the parent of the input/select/textarea
so in most cases .input
*/
.iconfield, /* generic */
.date_field,
.time_field,
.date_time_field,
.url_field{
.input{
position:relative;
&:before, &:after{
font-family:wagtail;
position:absolute;
top:0.5em;
line-height:100%;
font-size:2em;
color:$color-grey-3;
}
&:before{
left:0.3em;
}
&:after{
right:0.5em;
}
}
input:not([type=radio]), input:not([type=checkbox]), input:not([type=submit]), input:not([type=button]){
padding-left:2.5em;
}
&:before, &:after{
font-family:wagtail;
position:absolute;
top:0.4em;
font-size:1.4em;
color:$color-grey-3;
}
&:before{
left:0.5em;
}
&:after{
right:0.5em;
/* smaller fields required slight repositioning of icons */
&.field-small{
.input{
&:before, &:after{
font-size:1.5em;
top:0.3em;
}
&:before{
left:0.5em;
}
&:after{
right:0.5em;
}
}
}
/* special case for search spinners */
&.icon-spinner:after{
color:$color-teal;
opacity:0.8;
font-size:20px;
width:20px;
height:20px;
line-height:23px;
text-align:center;
top:0.3em;
}
}
.fields li{
.date_field,
.date_time_field{
.input:before{
@extend .icon-date:before;
}
}
.time_field{
.input:before{
@extend .icon-time:before;
}
}
.url_field{
.input:before{
@extend .icon-link:before;
}
}
/* This is specifically for model that are a generated set of checkboxes/radios */
.model_multiple_choice_field .input li,
.choice_field .input li{
label{
display:block;
width:auto;
float:none;
}
}
.fields > li,
.field-col{
@include clearfix();
padding-top:0.5em;
padding-bottom:1.2em;
}
.field-content .input li{
label{
width:auto;
float:none;
}
.field-row{
@include clearfix();
/* negative margin the bottom so it doesn't add too much space */
margin-bottom:-1.2em;
}
.input{
clear:both;
}
/* field sizing */
/* field sizing and alignment */
.field-small{
input, textarea, select, .richtext, .tagit{
@ -550,11 +628,6 @@ ul.inline li:first-child, li.inline:first-child{
.chosen { display: none; }
.unchosen { display: block; }
}
input[type=button]{
font-size:0.85em;
padding:0.5em 0.5em;
}
}
/* standard way of doing a chooser where the chosen object's title is overlayed */
@ -652,7 +725,7 @@ ul.tagit li.tagit-choice-editable{
/* search-bars */
.search-bar{
.required label:after{
.required .field > label:after{
display:none;
}
}
@ -674,6 +747,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
padding-top:1.2em;
padding-left:0;
.choice_field &,
.model_multiple_choice_field &,
.boolean_field &,
.model_choice_field &,
@ -681,26 +755,61 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
.file_field &{
padding-top:0;
}
}
.boolean_field &{
padding-bottom:0;
.label-above{
.field > label{
display:block;
padding:0 0 0.8em 0;
float:none;
width:auto;
}
}
input[type=submit], input[type=reset], input[type=button], .button, button{
font-size:0.95em;
padding:0.75em 1.4em;
padding:0 1.4em;
height: 3em;
line-height:3em;
&.button-small{
height:2.3em;
line-height:2.2em;
}
&.bicolor{
padding-left:3.5em;
&:before{
width:2.2em;
line-height:2.15em;
line-height:2.45em;
}
}
&.button-small.bicolor{
line-height:2.3em;
&:before{
line-height:1.85em;
}
}
}
/* Special styles to counteract Firefox's completely unwarranted assumptions about button styles */
input[type=submit], input[type=reset], input[type=button], button{
line-height:inherit;
&.button-small, &.bicolor,
&.button-small.bicolor{
line-height:inherit;
}
&.button-small{
height:2.3em;
}
}
.help{
opacity:1;
}
@ -712,8 +821,14 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
@include row();
}
.field-col{
float:left;
padding-left:0 !important;
}
.field-content{
@include column(10);
padding-right:0;
}
padding-left:0;
}
}

View file

@ -97,13 +97,6 @@ header{
}
}
/* mozilla specific hack */
@-moz-document url-prefix() {
.iconfield.icon-spinner:after{
line-height:20px;
}
}
.page-explorer header{
margin-bottom:0;
padding-bottom:0em;

View file

@ -96,7 +96,6 @@
.icon-unlocked:before {
content: "p";
}
.icon-doc-full-inverse:before {
content: "r";
}
@ -210,9 +209,9 @@
}
.icon-spinner:after{
width:1em;
animation: spin 1s infinite;
-webkit-animation: spin 1s infinite;
-moz-animation: spin 1s infinite;
animation: spin 0.5s infinite linear;
-webkit-animation: spin 0.5s infinite linear;
-moz-animation: spin 0.5s infinite linear;
content:"1";
}
.icon-pick:before{
@ -221,8 +220,10 @@
.icon-redirect:before{
content:"3";
}
/* Credit: Icon made by Zurb from Flaticon.com */
.icon-view:before{
content:"4"; /* Credit: Icon made by Zurb from Flaticon.com */
content:"4";
vertical-align:-4.5px;
font-size:1.3rem;
}
.icon-collapse-down:before{
@ -234,6 +235,9 @@
.icon-date:before{
content:"7";
}
.icon-time:before{
content:"8";
}
.icon-success:before{
content:"9";
}
@ -246,6 +250,7 @@
.icon-form:before{
content:"$";
}
.icon.text-replace{
font-size:0em;
line-height:0;

View file

@ -179,7 +179,6 @@ ul.listing{
@include clearfix();
margin-top:0.8em;
text-transform:uppercase;
font-size:0.85em;
margin-bottom:-0.5em;
a{
@ -197,7 +196,7 @@ ul.listing{
.button{
color:$color-teal;
border-color:$color-grey-3;
background:transparent;
background:white;
&:hover{
border-color:$color-teal;
@ -327,13 +326,11 @@ ul.listing{
opacity:0.7;
}
}
a{
color:auto;
}
.actions{
margin-top:1em;
}
.button{
background-color:transparent;
color:white;
border-color:$color-teal-darker;

View file

@ -1,32 +1,30 @@
.tab-nav{
@include clearfix();
@include row();
padding:0;
background:$color-grey-4;
li{
list-style-type:none;
width:48%;
width:33%;
float:left;
padding:0;
position:relative;
&:before,&:after{
display:none;
}
margin-right:1px;
}
a{
@include transition(border-color 0.2s ease);
background-color:lighten($color-teal-darker, 3%);
outline:none;
line-height:3em;
text-transform:uppercase;
font-weight:700;
font-size:1.2em;
text-decoration:none;
display:block;
padding:0 20px;
padding:0.7em;
color:white;
border-top:0.3em solid lighten($color-teal-darker, 3%);
border-bottom:1px solid transparent;
max-height:1.2em;
overflow:hidden;
&:hover{
color:white;
@ -45,7 +43,6 @@
min-width:0.9em;
color:white;
background:$color-red;
content:attr(data-count);
padding:0 0.3em;
line-height:1.4em;
@ -61,10 +58,21 @@
border-top:0.3em solid $color-grey-1;
}
li.settings a{
&:before{
font-family:wagtail;
vertical-align:middle;
text-transform:none;
content:"w";
margin-right:0.5em;
font-size:1.2em;
}
}
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
margin-top:0;
background-color:$color-header-bg;
}
}
.tab-content{
@ -79,14 +87,27 @@
}
@media screen and (min-width: $breakpoint-mobile){
.tab-nav li{
width:auto;
padding:0;
margin-left:0.7em;
}
.tab-nav{
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
}
.tab-nav a{
padding:0 50px;
li{
width:auto;
padding:0;
margin-left:0.7em;
}
a{
padding-left:$desktop-nice-padding - 10;
padding-right:$desktop-nice-padding - 10;
}
li.settings a{
padding-left:2em;
padding-right:2em;
}
}
.modal-content .tab-nav li{

View file

@ -394,11 +394,6 @@ footer{
.actions{
width:250px;
margin-right:$grid-gutter-width/2;
.button{
padding-top:1em;
padding-bottom:1em;
}
}
.meta{

View file

@ -1,6 +1,29 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M632.913 707.493l-173.647-173.649v-232.782h105.469v189.094l142.759 142.757zM512 90.125c-232.995 0-421.875 188.88-421.875 421.875s188.88 421.875 421.875 421.875 421.875-188.88 421.875-421.875-188.88-421.875-421.875-421.875zM512 828.406c-174.747 0-316.406-141.659-316.406-316.406s141.659-316.406 316.406-316.406c174.747 0 316.406 141.659 316.406 316.406s-141.659 316.406-316.406 316.406z"
],
"tags": [
"clock",
"time",
"schedule"
],
"grid": 16
},
"properties": {
"id": 72,
"order": 9,
"prevSize": 32,
"code": 56,
"name": "clock",
"ligatures": ""
},
"setIdx": 0,
"iconIdx": 72
},
{
"icon": {
"paths": [
@ -20,7 +43,7 @@
"name": "lock39copy",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 0
},
{
@ -42,7 +65,7 @@
"name": "lock39-open",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 1
},
{
@ -63,7 +86,7 @@
"name": "form",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 2
},
{
@ -82,7 +105,7 @@
"name": "uni61",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 3
},
{
@ -101,7 +124,7 @@
"name": "uni62",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 4
},
{
@ -120,7 +143,7 @@
"name": "uni63",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 5
},
{
@ -139,7 +162,7 @@
"name": "uni64",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 6
},
{
@ -158,7 +181,7 @@
"name": "uni65",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 7
},
{
@ -177,7 +200,7 @@
"name": "uni66",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 8
},
{
@ -196,7 +219,7 @@
"name": "uni67",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 9
},
{
@ -215,7 +238,7 @@
"name": "uni69",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 10
},
{
@ -234,7 +257,7 @@
"name": "uni6A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 11
},
{
@ -253,7 +276,7 @@
"name": "uni6B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 12
},
{
@ -272,7 +295,7 @@
"name": "uni6C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 13
},
{
@ -291,7 +314,7 @@
"name": "uni6E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 14
},
{
@ -310,7 +333,7 @@
"name": "uni68",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 15
},
{
@ -329,7 +352,7 @@
"name": "uni6F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 16
},
{
@ -348,7 +371,7 @@
"name": "uni71",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 17
},
{
@ -367,7 +390,7 @@
"name": "uni72",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 18
},
{
@ -386,7 +409,7 @@
"name": "uni73",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 19
},
{
@ -405,7 +428,7 @@
"name": "uni74",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 20
},
{
@ -424,7 +447,7 @@
"name": "uni75",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 21
},
{
@ -443,7 +466,7 @@
"name": "uni76",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 22
},
{
@ -462,7 +485,7 @@
"name": "uni77",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 23
},
{
@ -481,7 +504,7 @@
"name": "uni78",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 24
},
{
@ -500,7 +523,7 @@
"name": "uni7A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 25
},
{
@ -519,7 +542,7 @@
"name": "uni41",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 26
},
{
@ -538,7 +561,7 @@
"name": "uni42",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 27
},
{
@ -557,7 +580,7 @@
"name": "uni44",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 28
},
{
@ -576,7 +599,7 @@
"name": "uni43",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 29
},
{
@ -595,7 +618,7 @@
"name": "uni45",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 30
},
{
@ -614,7 +637,7 @@
"name": "uni46",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 31
},
{
@ -633,7 +656,7 @@
"name": "uni47",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 32
},
{
@ -652,7 +675,7 @@
"name": "uni48",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 33
},
{
@ -671,7 +694,7 @@
"name": "uni49",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 34
},
{
@ -690,7 +713,7 @@
"name": "uni4A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 35
},
{
@ -709,7 +732,7 @@
"name": "uni4B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 36
},
{
@ -728,7 +751,7 @@
"name": "uni4C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 37
},
{
@ -747,7 +770,7 @@
"name": "uni4D",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 38
},
{
@ -766,7 +789,7 @@
"name": "uni4E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 39
},
{
@ -785,7 +808,7 @@
"name": "uni4F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 40
},
{
@ -804,7 +827,7 @@
"name": "uni50",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 41
},
{
@ -823,7 +846,7 @@
"name": "uni51",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 42
},
{
@ -842,7 +865,7 @@
"name": "uni79",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 43
},
{
@ -861,7 +884,7 @@
"name": "uni52",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 44
},
{
@ -880,7 +903,7 @@
"name": "uni54",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 45
},
{
@ -899,7 +922,7 @@
"name": "uni57",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 46
},
{
@ -918,7 +941,7 @@
"name": "uni58",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 47
},
{
@ -937,7 +960,7 @@
"name": "uni59",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 48
},
{
@ -956,7 +979,7 @@
"name": "uni5A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 49
},
{
@ -975,7 +998,7 @@
"name": "uni56",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 50
},
{
@ -994,7 +1017,7 @@
"name": "uni31",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 51
},
{
@ -1013,7 +1036,7 @@
"name": "uni55",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 52
},
{
@ -1032,7 +1055,7 @@
"name": "uni33",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 53
},
{
@ -1051,7 +1074,7 @@
"name": "uni32",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 54
},
{
@ -1070,7 +1093,7 @@
"name": "uni35",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 55
},
{
@ -1089,7 +1112,7 @@
"name": "uni36",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 56
},
{
@ -1108,7 +1131,7 @@
"name": "uni30",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 57
},
{
@ -1127,7 +1150,7 @@
"name": "uni3F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 58
},
{
@ -1146,7 +1169,7 @@
"name": "uni21",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 59
},
{
@ -1165,7 +1188,7 @@
"name": "uni39",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 60
},
{
@ -1184,7 +1207,7 @@
"name": "uni53",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 61
},
{
@ -1203,7 +1226,7 @@
"name": "uni34",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 62
},
{
@ -1222,7 +1245,7 @@
"name": "uni37",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 63
}
],
@ -1245,13 +1268,14 @@
"baseline": 6.25,
"whitespace": 50
},
"showMetadata": false,
"showMetrics": true,
"useClassSelector": false,
"classSelector": ".icon",
"embed": false
"resetPoint": 58880
},
"imagePref": {
"color": 0,
"height": 32,
"columns": 16,
"margin": 16
},
"imagePref": {},
"historySize": 100,
"showCodes": true,
"search": "",

View file

@ -17,6 +17,7 @@
<glyph unicode="&#x35;" d="M135 424h241v-23h-241zM405 247l-127-124v222h-45v-220l-125 122-33-32 181-181 181 181z" />
<glyph unicode="&#x36;" d="M136 424h241v-23h-241zM108 122l126 124v-222h45v220l126-122 32 32-181 181-181-181z" />
<glyph unicode="&#x37;" d="M387.836 13.063h-263.672c-43.671 0-79.102 35.431-79.102 79.101v263.672c0 34.607 22.248 63.446 52.734 74.158v-34.607c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v39.551h158.203v-39.551c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v34.607c30.487-10.712 52.735-39.551 52.735-74.158v-263.672c0-43.671-35.431-79.101-79.101-79.101zM414.203 303.101h-316.406v-210.938c0-14.832 11.535-26.367 26.367-26.367h263.672c14.832 0 26.367 11.536 26.367 26.367zM308.735 171.265h52.735v-52.735h-52.735zM308.735 250.367h52.735v-52.734h-52.735zM229.633 171.265h52.734v-52.735h-52.734zM229.633 250.367h52.734v-52.734h-52.734zM150.531 171.265h52.734v-52.735h-52.734zM150.531 250.367h52.734v-52.734h-52.734zM374.652 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.183-13.184zM137.347 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.184-13.184z" />
<glyph unicode="&#x38;" d="M316.457 126.253l-86.823 86.825v116.391h52.734v-94.547l71.38-71.379zM256 434.938c-116.498 0-210.938-94.44-210.938-210.938s94.44-210.938 210.938-210.938 210.938 94.44 210.938 210.938-94.44 210.938-210.938 210.938zM256 65.797c-87.374 0-158.203 70.829-158.203 158.203s70.829 158.203 158.203 158.203c87.374 0 158.203-70.829 158.203-158.203s-70.829-158.203-158.203-158.203z" />
<glyph unicode="&#x39;" d="M256 449c-123.926 0-225-101.074-225-225s101.074-225 225-225c123.926 0 225 101.074 225 225s-101.074 225-225 225zM220.844 120.289l-102.832 103.711 39.551 39.551 63.281-64.16 135.351 135.351 39.551-39.551z" />
<glyph unicode="&#x3f;" d="M253.188 445.25c60.938 0 112.5-20.625 156.563-62.813 43.125-42.188 65.625-93.75 67.5-154.688 0-60.938-20.625-113.438-63.75-156.563-42.188-44.063-93.75-66.563-154.688-68.438-60.938 0-113.438 20.625-156.563 63.75-44.063 42.188-66.563 93.75-67.5 154.688s19.688 113.438 62.813 156.563c43.125 44.063 94.688 66.563 155.625 67.5zM252.25 89.938c9.375 0 17.813 2.813 23.438 8.438 5.625 6.563 9.375 14.063 9.375 22.5 0 10.313-1.875 17.813-8.438 24.375-5.625 5.625-14.063 8.438-23.438 8.438 0 0-0.938 0-0.938 0-9.375 0-16.875-2.813-22.5-8.438-6.563-5.625-9.375-13.125-10.313-22.5 0-9.375 2.813-16.875 9.375-23.438 5.625-5.625 13.125-9.375 22.5-9.375 0 0 0.938 0 0.938 0zM331.938 247.438c8.438 10.313 12.188 22.5 12.188 37.5 0 24.375-8.438 43.125-25.313 55.313s-38.438 17.813-64.688 17.813c-20.625 0-37.5-3.75-49.688-12.188-22.5-13.125-33.75-36.563-34.688-70.313 0 0 0-1.875 0-1.875s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 8.438 2.813 16.875 7.5 26.25 5.625 7.5 14.063 11.25 26.25 11.25 13.125 0 21.563-2.813 25.313-9.375 4.688-6.563 7.5-13.125 7.5-21.563 0-5.625-2.813-12.188-7.5-18.75-2.813-3.75-6.563-7.5-10.313-9.375 0 0-2.813-1.875-2.813-1.875-1.875-1.875-3.75-3.75-7.5-5.625-2.813-1.875-6.563-4.688-9.375-7.5-3.75-1.875-6.563-4.688-10.313-7.5s-6.563-5.625-8.438-8.438c-3.75-6.563-6.563-18.75-8.438-37.5 0 0 0-3.75 0-3.75s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 3.75 0 8.438 1.875 13.125 1.875 6.563 5.625 12.188 13.125 17.813 0 0 13.125 8.438 13.125 8.438 15 11.25 23.438 18.75 27.188 24.375z" />
<glyph unicode="&#x41;" d="M232 109l176 175c3 4 5 8 5 13s-2 9-5 13l-29 29c-4 4-8 6-13 6s-10-2-13-6l-134-133-60 60c-3 4-8 5-13 5s-9-1-13-5l-29-29c-3-4-5-8-5-13s2-9 5-13l103-102c3-4 7-6 12-6s10 2 13 6zM475 361v-274c0-23-8-42-24-58s-35-24-58-24h-274c-23 0-42 8-58 24s-24 35-24 58v274c0 23 8 42 24 58s35 24 58 24h274c23 0 42-8 58-24s24-35 24-58z" />

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -46,6 +46,13 @@ header{
}
}
a{
position:relative;
display:block;
width:100%;
height:100%;
}
span{
font-family:Bitter, Georgia, serif;
display:block;

View file

@ -44,6 +44,7 @@ form{
}
}
label{
width:auto;
color:white;
}
input[type=submit]{
@ -83,24 +84,9 @@ form{
.field{
padding:0;
}
.iconfield:before{
.iconfield .input:before{
display:none;
}
.full label{
@include border-radius(2px);
text-transform:uppercase;
padding:2px 5px;
position:absolute;
top:0;
left:0;
margin-top:-1px;
font-size:0.7em;
z-index:1;
margin:0.2em 0;
line-height:1.5em;
font-weight:normal;
}
}
/* Special full-width, one-off fields i.e a single text or textarea input */
.full input{
@ -173,7 +159,7 @@ form{
margin:0px (-$desktop-nice-padding);
.iconfield{
&:before{
.input:before{
display:inline-block;
position: absolute;
color:$color-grey-4;

View file

@ -25,9 +25,6 @@
}
}
.objects{
background:url("#{$static-root}bg-dark-diag.svg");
}
.object{
position:relative;
overflow:hidden;
@ -80,9 +77,9 @@
> h2, &.single-field label{
-webkit-font-smoothing: auto;
background:$color-grey-3;
background:$color-salmon-light;
text-transform:uppercase;
padding:0.9em 0 0.9em 4em;
padding:0.9em 0 0.9em 4.1em;
font-size:0.95em;
margin:0 0 0.2em 0;
line-height:1.5em;
@ -92,10 +89,10 @@
left:0;
right:0;
z-index:1;
text-shadow:1px 1px 1px rgba(255,255,255,0.5);
@include box-shadow(0 0 7px 0 rgba(0,0,0,0.4));
overflow:hidden;
&:before{
text-shadow:none;
font-family:wagtail;
text-transform:none;
content:"q";
@ -108,10 +105,11 @@
line-height:1.8em;
left:0px;
width:1.7em;
opacity:0.15;
color:white;
padding:0;
margin:0;
cursor:pointer;
background-color:$color-salmon;
}
}
@ -186,6 +184,17 @@
}
}
/* special panel for the publishing fields, requires a bit more pizzazz */
&.publishing{
h2:before{
content:"7";
font-size:2.4em;
line-height:1.4em;
width:1.4em;
}
}
&.title input,
&.title textarea{
font-size:2em;
@ -235,20 +244,20 @@
top:0px;
left:0px;
width:3.3em;
background-color:$color-teal;
padding:0;
margin:0 0 0 -20px;
cursor:pointer;
a{
font-size: 0em;
line-height: 0;
.button{
@include border-radius(0);
overflow: visible;
display:block;
display:inline-block;
padding:0;
width:3.45em;
background-color:$color-salmon;
&:before{
position:relative;
color:white;
padding:0;
line-height:1.8em; /* specific height required as parent 'a' has no height */
font-size:1.4rem;

View file

@ -68,6 +68,12 @@ section{
.color-grey-5{
background-color:$color-grey-5;
}
.color-salmon{
background-color:$color-salmon;
}
.color-salmon-light{
background-color:$color-salmon-light;
}
}

View file

@ -3,10 +3,13 @@
.hallotoolbar{
position:absolute;
left:$mobile-nice-padding;
z-index:5;
margin-top:4em;
margin-left:0em;
margin-left:1.2em;
}
/* full is added to hallotoolbar when invoked on a field set to the full layout style */
.hallotoolbar.full{
margin-left:$mobile-nice-padding;
}
.hallotoolbar.affixed{
position:fixed;
@ -14,6 +17,7 @@
}
.hallotoolbar button{
@include border-radius(0);
height:2.4em;
}
.richtext {
@ -149,7 +153,7 @@
}
@media screen and (min-width: $breakpoint-mobile){
.hallotoolbar{
left:$menu-width + $desktop-nice-padding;
.hallotoolbar.full{
margin-left:$desktop-nice-padding;
}
}

View file

@ -27,9 +27,11 @@ $breakpoint-desktop-larger: 100em; /* 1600px */
$color-teal: #43b1b0;
$color-teal-darker: darken($color-teal, 10%);
$color-teal-dark: #246060;
$color-red: #f7474e;
$color-red: #cd3238;
$color-orange:#e9b04d;
$color-green: #189370;
$color-salmon: #f37e77;
$color-salmon-light: #fcf2f2;
/* darker to lighter */
$color-grey-1: #333333;

View file

@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model
from django.db.models import Q
from wagtail.wagtailcore.models import PageRevision, GroupPagePermission
from wagtail.wagtailusers.models import UserProfile
# The following will check to see if we can import task from celery -
# if not then we definitely haven't installed it
@ -53,7 +54,7 @@ def send_notification(page_revision_id, notification, excluded_user_id):
if notification == 'submitted':
# Get list of publishers
recipients = users_with_page_permission(revision.page, 'publish')
elif notification == 'approved' or notification == 'rejected':
elif notification in ['rejected', 'approved']:
# Get submitter
recipients = [revision.user]
else:
@ -62,7 +63,7 @@ def send_notification(page_revision_id, notification, excluded_user_id):
# Get list of email addresses
email_addresses = [
recipient.email for recipient in recipients
if recipient.email and recipient.id != excluded_user_id
if recipient.email and recipient.id != excluded_user_id and getattr(UserProfile.get_for_user(recipient), notification + '_notifications')
]
# Return if there are no email addresses

View file

@ -1,5 +1,7 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Account" %}{% endblock %}
{% block content %}
{% trans "Account" as account_str %}
{% include "wagtailadmin/shared/header.html" with title=account_str %}
@ -28,6 +30,17 @@
</small>
</li>
{% endif %}
{% if show_notification_preferences %}
<li class="row row-flush">
<div class="col6">
<a href="{% url 'wagtailadmin_account_notification_preferences' %}" class="button button-primary">{% trans "Notification preferences" %}</a>
</div>
<small class="col6">
{% trans "Choose which email notifications to receive." %}
</small>
</li>
{% endif %}
</ul>
</div>
{% endblock %}
{% endblock %}

View file

@ -1,5 +1,7 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Change password" %}{% endblock %}
{% block content %}
{% trans "Change password" as change_str %}
{% include "wagtailadmin/shared/header.html" with title=change_str %}

View file

@ -0,0 +1,20 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Notification Preferences" %}{% endblock %}
{% block content %}
{% trans "Notification Preferences" as prefs_str %}
{% include "wagtailadmin/shared/header.html" with title=prefs_str %}
<div class="nice-padding">
<form action="{% url 'wagtailadmin_account_notification_preferences' %}" method="POST">
{% csrf_token %}
<ul class="fields">
{% for field in form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
<li class="submit"><input type="submit" value="{% trans 'Update' %}" /></li>
</ul>
</form>
</div>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% if not is_searching %}
<h2>{% trans "Explorer" %}</h2>
{% include "wagtailadmin/shared/breadcrumb.html" with page=parent_page %}
{% include "wagtailadmin/shared/breadcrumb.html" with page=parent_page choosing=1 %}
{% else %}
<h2>

View file

@ -1,4 +1,4 @@
{% extends "wagtailadmin/edit_handlers/field_panel_field.html" %}
{% extends "wagtailadmin/shared/field.html" %}
{% load i18n %}
{% comment %}
Either the chosen or unchosen div will be shown, depending on the presence
@ -17,14 +17,14 @@
<div class="actions">
{% if not field.field.required %}
<input type="button" class="button-secondary action-clear" value="{% block clear_button_label %}{% trans "Clear choice" %}{% endblock %}">
<input type="button" class="action-clear button-small button-secondary" value="{% block clear_button_label %}{% trans "Clear choice" %}{% endblock %}">
{% endif %}
<input type="button" class="button-secondary action-choose" value="{% block choose_another_button_label %}{% trans "Choose another item" %}{% endblock %}">
<input type="button" class="action-choose button-small button-secondary" value="{% block choose_another_button_label %}{% trans "Choose another item" %}{% endblock %}">
</div>
</div>
<div class="unchosen">
<input type="button" class="action-choose button-secondary" value="{% block choose_button_label %}{% trans "Choose an item" %}{% endblock %}">
<input type="button" class="action-choose button-small button-secondary" value="{% block choose_button_label %}{% trans "Choose an item" %}{% endblock %}">
</div>
</div>

View file

@ -1,23 +1 @@
<div class="field">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
{% include "wagtailadmin/shared/field.html" %}

View file

@ -0,0 +1,7 @@
<ul class="field-row {{ self.classes|join:" " }}">
{% for child in self.children %}
<li class="field-col {{ child.classes|join:" " }}">
{{ child.render_as_field }}
</li>
{% endfor %}
</ul>

View file

@ -2,9 +2,7 @@
<legend>{{ self.heading }}</legend>
<ul class="fields">
{% for child in self.children %}
<li {% if child.field_classnames %}class="{{ child.field_classnames }}"{% endif %}>
{{ child.render_as_field }}
</li>
<li class="{{ child.classes|join:" " }}">{{ child.render_as_field }}</li>
{% endfor %}
</ul>
</fieldset>

View file

@ -1,6 +1,6 @@
<ul class="objects">
{% for child in self.children %}
<li class="object {{ child.object_classnames }}">
<li class="object {{ child.classes|join:" " }}">
{% if child.heading %}
<h2>{{ child.heading }}</h2>
{% endif %}

View file

@ -1,6 +1,6 @@
<fieldset>
<legend>{{ self.heading }}</legend>
<ul class="fields">
<li class="{{ self.field_classnames }}">{{ field_content }}</li>
<li>{{ field_content }}</li>
</ul>
</fieldset>

View file

@ -1,12 +1,12 @@
<ul class="tab-nav merged">
{% for child in self.children %}
<li class="{% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
<li class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for child in self.children %}
<section id="{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">
<section id="{{ child.heading|slugify }}" class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}">
{{ child.render_as_object }}
</section>
{% endfor %}

View file

@ -3,25 +3,31 @@
<h2 class="visuallyhidden">{% trans "Site summary" %}</h2>
<ul class="stats">
<li class="icon icon-doc-empty-inverse">
<a href="{% url 'wagtailadmin_explore_root' %}">
{% blocktrans count counter=total_pages %}
<span>{{ total_pages }}</span> Page
{% plural %}
<span>{{ total_pages }}</span> Pages
{% endblocktrans %}
</a>
</li>
<li class="icon icon-image">
<a href="{% url 'wagtailimages_index' %}">
{% blocktrans count counter=total_images %}
<span>{{ total_images }}</span> Image
{% plural %}
<span>{{ total_images }}</span> Images
{% endblocktrans %}
</a>
</li>
<li class="icon icon-doc-full-inverse">
<a href="{% url 'wagtaildocs_index' %}">
{% blocktrans count counter=total_docs %}
<span>{{ total_docs }}</span> Document
{% plural %}
<span>{{ total_docs }}</span> Documents
{% endblocktrans %}
</a>
</li>
</ul>
</section>
</section>

View file

@ -28,17 +28,17 @@
<ul class="fields">
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.username.label_tag }}
<div class="input iconfield icon-user">
<div class="input icon-user">
{{ form.username }}
</div>
</div>
</li>
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.password.label_tag }}
<div class="input iconfield icon-password">
<div class="input icon-password">
{{ form.password }}
</div>
</div>

View file

@ -1,4 +1,4 @@
{% load i18n %}{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been approved{% endblocktrans %}
{% extends 'wagtailadmin/notifications/base_notification.html' %}{% block notification %}{% load i18n %}{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been approved{% endblocktrans %}
{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been approved.{% endblocktrans %}
{% trans "You can view the page here:" %} {{ revision.page.full_url }}
{% trans "You can view the page here:" %} {{ revision.page.full_url }}{% endblock %}

View file

@ -0,0 +1,3 @@
{% load i18n %}{% block notification %}{% endblock %}
{% trans "Edit your notification preferences here:" %} {{ settings.BASE_URL }}{% url 'wagtailadmin_account_notification_preferences' %}

View file

@ -1,4 +1,4 @@
{% load i18n %}{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been rejected{% endblocktrans %}
{% extends 'wagtailadmin/notifications/base_notification.html' %}{% block notification %}{% load i18n %}{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been rejected{% endblocktrans %}
{% blocktrans with title=revision.page.title|safe %}The page "{{ title }}" has been rejected.{% endblocktrans %}
{% trans "You can edit the page here:"%} {{ settings.BASE_URL }}{% url 'wagtailadmin_pages_edit' revision.page.id %}
{% trans "You can edit the page here:"%} {{ settings.BASE_URL }}{% url 'wagtailadmin_pages_edit' revision.page.id %}{% endblock %}

View file

@ -1,5 +1,5 @@
{% load i18n %}{% blocktrans with page=revision.page|safe %}The page "{{ page }}" has been submitted for moderation{% endblocktrans %}
{% extends 'wagtailadmin/notifications/base_notification.html' %}{% block notification %}{% load i18n %}{% blocktrans with page=revision.page|safe %}The page "{{ page }}" has been submitted for moderation{% endblocktrans %}
{% blocktrans with page=revision.page|safe %}The page "{{ page }}" has been submitted for moderation.{% endblocktrans %}
{% trans "You can preview the page here:" %} {{ settings.BASE_URL }}{% url 'wagtailadmin_pages_preview_for_moderation' revision.id %}
{% trans "You can edit the page here:" %} {{ settings.BASE_URL }}{% url 'wagtailadmin_pages_edit' revision.page.id %}
{% trans "You can edit the page here:" %} {{ settings.BASE_URL }}{% url 'wagtailadmin_pages_edit' revision.page.id %}{% endblock %}

View file

@ -16,11 +16,9 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/tag-it.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/expanding_formset.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/modal-workflow.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtail-toolbar.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-hr.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-editor.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-chooser.js"></script>
<script src="{{ STATIC_URL }}admin/js/urlify.js"></script>

View file

@ -1,4 +1,4 @@
<button class="action-preview button {% if icon %}icon icon-view{% endif %}"
<button class="action-preview {% if icon %}icon icon-view{% endif %}"
data-action="{% url 'wagtailadmin_pages_preview_on_create' content_type.app_label content_type.model parent_page.id %}{% if mode %}?mode={{ mode|urlencode }}{% endif %}"
data-placeholder="{% url 'wagtailadmin_pages_preview' %}"
data-windowname="wagtail_preview_{{ parent_page.id }}_child">{{ label }}</button>

View file

@ -1,4 +1,4 @@
<button class="action-preview button {% if icon %}icon icon-view{% endif %}"
<button class="action-preview {% if icon %}icon icon-view{% endif %}"
data-action="{% url 'wagtailadmin_pages_preview_on_edit' page.id %}{% if mode %}?mode={{ mode|urlencode }}{% endif %}"
data-placeholder="{% url 'wagtailadmin_pages_preview' %}"
data-windowname="wagtail_preview_{{ page.id }}">{{ label }}</button>

View file

@ -11,5 +11,6 @@
<div id="loading-spinner-wrapper">
<div id="loading-spinner"></div>
</div>
<iframe id="preview-frame" src="{% url 'wagtailadmin_pages_preview_loading' %}"></iframe>
</body>
</html>

View file

@ -3,12 +3,12 @@
<ul class="breadcrumb">
{% for page in page.get_ancestors %}
{% if page.is_root %}
<li class="home"><a href="{% url 'wagtailadmin_explore_root' %}" class="icon icon-home text-replace">{% trans 'Home' %}</a></li>
<li class="home"><a href="{% if choosing %}{% url 'wagtailadmin_choose_page_child' page.id %}?{{ querystring }}{% else %}{% url 'wagtailadmin_explore_root' %}{% endif %}" class="{% if choosing %}navigate-pages{% endif %} icon icon-home text-replace">{% trans 'Home' %}</a></li>
{% else %}
<li><a href="{% url 'wagtailadmin_explore' page.id %}">{{ page.title }}</a></li>
<li><a href="{% if choosing %}{% url 'wagtailadmin_choose_page_child' page.id %}?{{ querystring }}{% else %}{% url 'wagtailadmin_explore' page.id %}{% endif %}" {% if choosing %}class="navigate-pages"{% endif %}>{{ page.title }}</a></li>
{% endif %}
{% endfor %}
{% if include_self %}
<li><a href="{% url 'wagtailadmin_explore' page.id %}">{{ page.title }}</a></li>
<li><a href="{% if choosing %}{% url 'wagtailadmin_choose_page_child' page.id %}?{{ querystring }}{% else %}{% url 'wagtailadmin_explore' page.id %}{% endif %}" {% if choosing %}class="navigate-pages"{% endif %}>{{ page.title }}</a></li>
{% endif %}
</ul>

View file

@ -0,0 +1,25 @@
{% load wagtailadmin_tags %}
<div class="field {{ field.field_classnames }} {{ field|fieldtype }} {{ field_classes }}">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ field.input_classnames }} {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
{# This span only used on rare occasions by certain types of input #}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>

View file

@ -1,25 +1,4 @@
{% load wagtailadmin_tags %}
<li class="{% if field.field.required %}required{% endif %} {{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<div class="field">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
<li class="{% if field.field.required %}required{% endif %} {{ wrapper_classes }} {{ li_classes }} {% if field.errors %}error{% endif %}">
{% include "wagtailadmin/shared/field.html" %}
</li>

View file

@ -8,7 +8,7 @@
<form class="col search-form" action="{% url search_url %}" method="get">
<ul class="fields">
{% for field in search_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-search" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" input_classes="icon-search" %}
{% endfor %}
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
</ul>

View file

@ -1,10 +1,7 @@
<!doctype html>
{% load compress %}
<!--[if lt IE 7]> <html class="no-js ie lt-ie9 lt-ie8 lt-ie7" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie lt-ie9 lt-ie8" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie lt-ie9" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 9]> <html class="no-js ie lt-ie10" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if gt IE 9]><!--> <html class="no-js" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <!--<![endif]-->
<!--[if lt IE 9]> <html class="no-js ie lt-ie9" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 9]> <html class="no-js ie lt-ie10" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

View file

@ -1,10 +1,12 @@
from django.test import TestCase
from wagtail.tests.utils import unittest, WagtailTestUtils
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import mail
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailusers.models import UserProfile
class TestAuthentication(TestCase, WagtailTestUtils):
"""
@ -177,6 +179,97 @@ class TestAccountSection(TestCase, WagtailTestUtils):
# Check that the password was not changed
self.assertTrue(User.objects.get(username='test').check_password('password'))
def test_notification_preferences_view(self):
"""
This tests that the notification preferences view responds with the
notification preferences page
"""
# Get notification preferences page
response = self.client.get(reverse('wagtailadmin_account_notification_preferences'))
# Check that the user recieved a notification preferences page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/notification_preferences.html')
def test_notification_preferences_view_post(self):
"""
This posts to the notification preferences view and checks that the
user's profile is updated
"""
# Post new values to the notification preferences page
post_data = {
'submitted_notifications': u'false',
'approved_notifications': u'false',
'rejected_notifications': u'true',
}
response = self.client.post(reverse('wagtailadmin_account_notification_preferences'), post_data)
# Check that the user was redirected to the account page
self.assertRedirects(response, reverse('wagtailadmin_account'))
profile = UserProfile.get_for_user(User.objects.get(username='test'))
# Check that the notification preferences are as submitted
self.assertFalse(profile.submitted_notifications)
self.assertFalse(profile.approved_notifications)
self.assertTrue(profile.rejected_notifications)
class TestAccountManagementForNonModerator(TestCase, WagtailTestUtils):
"""
Tests of reduced-functionality for editors
"""
def setUp(self):
# Create a non-moderator user
self.submitter = User.objects.create_user('submitter', 'submitter@example.com', 'password')
self.submitter.groups.add(Group.objects.get(name='Editors'))
self.client.login(username=self.submitter.username, password='password')
def test_notification_preferences_form_is_reduced_for_non_moderators(self):
"""
This tests that a user without publish permissions is not shown the
notification preference for 'submitted' items
"""
response = self.client.get(reverse('wagtailadmin_account_notification_preferences'))
self.assertIn('approved_notifications', response.context['form'].fields.keys())
self.assertIn('rejected_notifications', response.context['form'].fields.keys())
self.assertNotIn('submitted_notifications', response.context['form'].fields.keys())
class TestAccountManagementForAdminOnlyUser(TestCase, WagtailTestUtils):
"""
Tests for users with no edit/publish permissions at all
"""
def setUp(self):
# Create a non-moderator user
admin_only_group = Group.objects.create(name='Admin Only')
admin_only_group.permissions.add(Permission.objects.get(codename='access_admin'))
self.admin_only_user = User.objects.create_user('admin_only_user', 'admin_only_user@example.com', 'password')
self.admin_only_user.groups.add(admin_only_group)
self.client.login(username=self.admin_only_user.username, password='password')
def test_notification_preferences_view_redirects_for_admin_only_users(self):
"""
Test that the user is not shown the notification preferences view but instead
redirected to the account page
"""
response = self.client.get(reverse('wagtailadmin_account_notification_preferences'))
self.assertRedirects(response, reverse('wagtailadmin_account'))
def test_notification_preferences_link_not_shown_for_admin_only_users(self):
"""
Test that the user is not even shown the link to the notification
preferences view
"""
response = self.client.get(reverse('wagtailadmin_account'))
self.assertEqual(response.context['show_notification_preferences'], False)
self.assertNotContains(response, reverse('wagtailadmin_account_notification_preferences'))
# safety check that checking for absence/presence of urls works
self.assertContains(response, reverse('wagtailadmin_home'))
class TestPasswordReset(TestCase, WagtailTestUtils):
"""

View file

@ -94,14 +94,6 @@ class TestEditHandler(TestCase):
def test_edit_handler_init_no_form(self):
self.assertRaises(ValueError, EditHandler, instance=True)
def test_object_classnames(self):
result = self.edit_handler.object_classnames()
self.assertEqual(result, "")
def test_field_classnames(self):
result = self.edit_handler.field_classnames()
self.assertEqual(result, "")
def test_field_type(self):
result = self.edit_handler.field_type()
self.assertEqual(result, "")
@ -164,15 +156,6 @@ class TestTabbedInterface(TestCase):
self.assertTrue(issubclass(self.TabbedInterfaceClass,
BaseTabbedInterface))
def test_object_classnames_no_classname(self):
result = self.tabbed_interface.object_classnames()
self.assertEqual(result, 'multi-field')
def test_object_classnames(self):
self.tabbed_interface.classname = 'foo'
result = self.tabbed_interface.object_classnames()
self.assertEqual(result, 'multi-field foo')
def test_widget_overrides(self):
result = self.tabbed_interface.widget_overrides()
self.assertEqual(result, {'foo': 'bar'})
@ -217,15 +200,6 @@ class TestBaseFieldPanel(TestCase):
instance=True,
form={'barbecue': fake_field})
def test_object_classnames_no_classname(self):
result = self.base_field_panel.object_classnames()
self.assertEqual(result, "single-field")
def test_object_classnames(self):
self.base_field_panel.classname = "bar"
result = self.base_field_panel.object_classnames()
self.assertEqual(result, "single-field bar")
def test_field_type(self):
fake_object = self.FakeClass()
another_fake_object = self.FakeClass()
@ -233,16 +207,6 @@ class TestBaseFieldPanel(TestCase):
self.base_field_panel.bound_field = fake_object
self.assertEqual(self.base_field_panel.field_type(), 'fake_class')
def test_field_classnames(self):
fake_object = self.FakeClass()
another_fake_object = self.FakeClass()
another_fake_object.required = True
fake_object.errors = True
fake_object.field = another_fake_object
self.base_field_panel.bound_field = fake_object
self.assertEqual(self.base_field_panel.field_classnames(),
'fake_class required error')
class TestFieldPanel(TestCase):
class FakeClass(object):
@ -265,8 +229,6 @@ class TestFieldPanel(TestCase):
result = self.field_panel.render_as_object()
self.assertIn('<legend>label</legend>',
result)
self.assertIn('<li class="fake_class error">',
result)
self.assertIn('<p class="error-message">',
result)

View file

@ -1,11 +1,16 @@
from datetime import timedelta
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, StandardChild, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Permission
from django.core import mail
from django.core.paginator import Paginator
from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, StandardChild, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailusers.models import UserProfile
class TestPageExplorer(TestCase, WagtailTestUtils):
@ -167,6 +172,61 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
def test_create_simplepage_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check the scheduled times
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
self.assertTrue(page.status_string, "draft")
# No revisions with approved_go_live_at
self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
def test_create_simplepage_scheduled_go_live_before_expiry(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
def test_create_simplepage_scheduled_expire_in_the_past(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_create_simplepage_post_publish(self):
post_data = {
'title': "New page!",
@ -185,6 +245,34 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
def test_create_simplepage_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
# A revision with approved_go_live_at should exist now
self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
# But Page won't be live
self.assertFalse(page.live)
self.assertTrue(page.status_string, "scheduled")
def test_create_simplepage_post_submit(self):
# Create a moderator user for testing email
moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')
@ -243,7 +331,6 @@ class TestPageCreation(TestCase, WagtailTestUtils):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', 100000)))
self.assertEqual(response.status_code, 404)
@unittest.expectedFailure # FIXME: Crashes!
def test_create_nonpagetype(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id)))
self.assertEqual(response.status_code, 404)
@ -325,6 +412,63 @@ class TestPageEdit(TestCase, WagtailTestUtils):
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertTrue(child_page_new.has_unpublished_changes)
def test_edit_post_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page will still be live
self.assertTrue(child_page_new.live)
# A revision with approved_go_live_at should not exist
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
# But a revision with go_live_at and expire_at in their content json *should* exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_at.date())).exists())
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists())
def test_edit_scheduled_go_live_before_expiry(self):
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
def test_edit_scheduled_expire_in_the_past(self):
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_page_edit_post_publish(self):
# Tests publish from edit page
post_data = {
@ -345,6 +489,77 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
def test_edit_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should not be live anymore
self.assertFalse(child_page_new.live)
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
def test_edit_post_publish_now_an_already_scheduled(self):
# First let's publish a page with a go_live_at in the future
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should not be live anymore
self.assertFalse(child_page_new.live)
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
# Now, let's edit it and publish it right now
go_live_at = timezone.now()
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': "",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should be live now
self.assertTrue(child_page_new.live)
# And a revision with approved_go_live_at should not exist
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
def test_page_edit_post_submit(self):
# Create a moderator user for testing email
moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')
@ -651,11 +866,6 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
# Page must be live
self.assertTrue(Page.objects.get(id=self.page.id).live)
# Submitter must recieve an approved email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been approved')
def test_approve_moderation_view_bad_revision_id(self):
"""
This tests that the approve moderation view handles invalid revision ids correctly
@ -705,11 +915,6 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
# Revision must no longer be submitted for moderation
self.assertFalse(PageRevision.objects.get(id=self.revision.id).submitted_for_moderation)
# Submitter must recieve a rejected email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been rejected')
def test_reject_moderation_view_bad_revision_id(self):
"""
This tests that the reject moderation view handles invalid revision ids correctly
@ -855,3 +1060,136 @@ class TestSubpageBusinessRules(TestCase, WagtailTestUtils):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.business_subindex.id, )))
# BusinessChild is the only valid subpage type of BusinessSubIndex, so redirect straight there
self.assertRedirects(response, reverse('wagtailadmin_pages_create', args=('tests', 'businesschild', self.business_subindex.id)))
class TestNotificationPreferences(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
self.user = self.login()
# Create two moderator users for testing 'submitted' email
self.moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')
self.moderator2 = User.objects.create_superuser('moderator2', 'moderator2@email.com', 'password')
# Create a submitter for testing 'rejected' and 'approved' emails
self.submitter = User.objects.create_user('submitter', 'submitter@email.com', 'password')
# User profiles for moderator2 and the submitter
self.moderator2_profile = UserProfile.get_for_user(self.moderator2)
self.submitter_profile = UserProfile.get_for_user(self.submitter)
# Create a page and submit it for moderation
self.child_page = SimplePage(
title="Hello world!",
slug='hello-world',
live=False,
)
self.root_page.add_child(instance=self.child_page)
# POST data to edit the page
self.post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-submit': "Submit",
}
def submit(self):
return self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), self.post_data)
def silent_submit(self):
"""
Sets up the child_page as needing moderation, without making a request
"""
self.child_page.save_revision(user=self.submitter, submitted_for_moderation=True)
self.revision = self.child_page.get_latest_revision()
def approve(self):
return self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
def reject(self):
return self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
def test_vanilla_profile(self):
# Check that the vanilla profile has rejected notifications on
self.assertEqual(self.submitter_profile.rejected_notifications, True)
# Check that the vanilla profile has approved notifications on
self.assertEqual(self.submitter_profile.approved_notifications, True)
def test_submit_notifications_sent(self):
# Submit
self.submit()
# Check that both the moderators got an email, and no others
self.assertEqual(len(mail.outbox), 1)
self.assertIn(self.moderator.email, mail.outbox[0].to)
self.assertIn(self.moderator2.email, mail.outbox[0].to)
self.assertEqual(len(mail.outbox[0].to), 2)
def test_submit_notification_preferences_respected(self):
# moderator2 doesn't want emails
self.moderator2_profile.submitted_notifications = False
self.moderator2_profile.save()
# Submit
self.submit()
# Check that only one moderator got an email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual([self.moderator.email], mail.outbox[0].to)
def test_approved_notifications(self):
# Set up the page version
self.silent_submit()
# Approve
self.approve()
# Submitter must recieve an approved email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been approved')
def test_approved_notifications_preferences_respected(self):
# Submitter doesn't want 'approved' emails
self.submitter_profile.approved_notifications = False
self.submitter_profile.save()
# Set up the page version
self.silent_submit()
# Approve
self.approve()
# No email to send
self.assertEqual(len(mail.outbox), 0)
def test_rejected_notifications(self):
# Set up the page version
self.silent_submit()
# Reject
self.reject()
# Submitter must recieve a rejected email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been rejected')
def test_rejected_notification_preferences_respected(self):
# Submitter doesn't want 'rejected' emails
self.submitter_profile.rejected_notifications = False
self.submitter_profile.save()
# Set up the page version
self.silent_submit()
# Reject
self.reject()
# No email to send
self.assertEqual(len(mail.outbox), 0)

View file

@ -50,6 +50,7 @@ urlpatterns += [
url(r'^pages/(\d+)/edit/preview/$', pages.preview_on_edit, name='wagtailadmin_pages_preview_on_edit'),
url(r'^pages/preview/$', pages.preview, name='wagtailadmin_pages_preview'),
url(r'^pages/preview_loading/$', pages.preview_loading, name='wagtailadmin_pages_preview_loading'),
url(r'^pages/(\d+)/view_draft/$', pages.view_draft, name='wagtailadmin_pages_view_draft'),
url(r'^pages/(\d+)/add_subpage/$', pages.add_subpage, name='wagtailadmin_pages_add_subpage'),
@ -77,6 +78,7 @@ urlpatterns += [
url(r'^login/$', account.login, name='wagtailadmin_login'),
url(r'^account/$', account.account, name='wagtailadmin_account'),
url(r'^account/change_password/$', account.change_password, name='wagtailadmin_account_change_password'),
url(r'^account/notification_preferences/$', account.notification_preferences, name='wagtailadmin_account_notification_preferences'),
url(r'^logout/$', account.logout, name='wagtailadmin_logout'),
url(r'^userbar/(\d+)/$', userbar.for_frontend, name='wagtailadmin_userbar_frontend'),

View file

@ -9,12 +9,19 @@ from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
from wagtail.wagtailadmin import forms
from wagtail.wagtailusers.forms import NotificationPreferencesForm
from wagtail.wagtailusers.models import UserProfile
from wagtail.wagtailcore.models import UserPagePermissionsProxy
@permission_required('wagtailadmin.access_admin')
def account(request):
user_perms = UserPagePermissionsProxy(request.user)
show_notification_preferences = user_perms.can_edit_pages() or user_perms.can_publish_pages()
return render(request, 'wagtailadmin/account/account.html', {
'show_change_password': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True) and request.user.has_usable_password(),
'show_notification_preferences': show_notification_preferences
})
@ -42,6 +49,29 @@ def change_password(request):
})
@permission_required('wagtailadmin.access_admin')
def notification_preferences(request):
if request.POST:
form = NotificationPreferencesForm(request.POST, instance=UserProfile.get_for_user(request.user))
if form.is_valid():
form.save()
messages.success(request, _("Your preferences have been updated successfully!"))
return redirect('wagtailadmin_account')
else:
form = NotificationPreferencesForm(instance=UserProfile.get_for_user(request.user))
# quick-and-dirty catch-all in case the form has been rendered with no
# fields, as the user has no customisable permissions
if not form.fields:
return redirect('wagtailadmin_account')
return render(request, 'wagtailadmin/account/notification_preferences.html', {
'form': form,
})
@sensitive_post_parameters()
@never_cache
def login(request):

View file

@ -5,12 +5,13 @@ from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.decorators import permission_required
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailadmin import tasks, hooks
from wagtail.wagtailadmin import tasks, hooks, signals
from wagtail.wagtailcore.models import Page, PageRevision
@ -115,12 +116,17 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
except ContentType.DoesNotExist:
raise Http404
# Get class
page_class = content_type.model_class()
# Make sure the class is a descendant of Page
if not issubclass(page_class, Page):
raise Http404
# page must be in the list of allowed subpage types for this parent ID
if content_type not in parent_page.clean_subpage_types():
raise PermissionDenied
page_class = content_type.model_class()
page = page_class(owner=request.user)
edit_handler_class = get_page_edit_handler(page_class)
form_class = edit_handler_class.get_form_class(page_class)
@ -136,21 +142,63 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
return slug
form.fields['slug'].clean = clean_slug
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
def clean():
cleaned_data = form_class.clean(form)
# Go live must be before expire
go_live_at = cleaned_data.get('go_live_at')
expire_at = cleaned_data.get('expire_at')
if go_live_at and expire_at:
if go_live_at > expire_at:
msg = _('Go live date/time must be before expiry date/time')
form._errors['go_live_at'] = form.error_class([msg])
form._errors['expire_at'] = form.error_class([msg])
del cleaned_data['go_live_at']
del cleaned_data['expire_at']
# Expire must be in the future
expire_at = cleaned_data.get('expire_at')
if expire_at and expire_at < timezone.now():
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
del cleaned_data['expire_at']
return cleaned_data
form.clean = clean
if form.is_valid():
page = form.save(commit=False) # don't save yet, as we need treebeard to assign tree params
is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage()
is_submitting = bool(request.POST.get('action-submit'))
go_live_at = form.cleaned_data.get('go_live_at')
future_go_live = go_live_at and go_live_at > timezone.now()
approved_go_live_at = None
if is_publishing:
page.live = True
page.has_unpublished_changes = False
page.expired = False
if future_go_live:
page.live = False
# Set approved_go_live_at only if is publishing
# and the future_go_live is actually in future
approved_go_live_at = go_live_at
else:
page.live = True
else:
page.live = False
page.has_unpublished_changes = True
parent_page.add_child(instance=page) # assign tree parameters - will cause page to be saved
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
# Pass approved_go_live_at to save_revision
page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
if is_publishing:
messages.success(request, _("Page '{0}' published.").format(page.title))
@ -167,9 +215,10 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
messages.error(request, _("The page could not be created due to errors."))
messages.error(request, _("The page could not be created due to validation errors"))
edit_handler = edit_handler_class(instance=page, form=form)
else:
signals.init_new_page.send(sender=create, page=page, parent=parent_page)
form = form_class(instance=page)
edit_handler = edit_handler_class(instance=page, form=form)
@ -209,15 +258,54 @@ def edit(request, page_id):
return slug
form.fields['slug'].clean = clean_slug
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
def clean():
cleaned_data = form_class.clean(form)
# Go live must be before expire
go_live_at = cleaned_data.get('go_live_at')
expire_at = cleaned_data.get('expire_at')
if go_live_at and expire_at:
if go_live_at > expire_at:
msg = _('Go live date/time must be before expiry date/time')
form._errors['go_live_at'] = form.error_class([msg])
form._errors['expire_at'] = form.error_class([msg])
del cleaned_data['go_live_at']
del cleaned_data['expire_at']
# Expire must be in the future
expire_at = cleaned_data.get('expire_at')
if expire_at and expire_at < timezone.now():
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
del cleaned_data['expire_at']
return cleaned_data
form.clean = clean
if form.is_valid():
is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish()
is_submitting = bool(request.POST.get('action-submit'))
go_live_at = form.cleaned_data.get('go_live_at')
future_go_live = go_live_at and go_live_at > timezone.now()
approved_go_live_at = None
if is_publishing:
page.live = True
page.has_unpublished_changes = False
page.expired = False
if future_go_live:
page.live = False
# Set approved_go_live_at only if publishing
approved_go_live_at = go_live_at
else:
page.live = True
form.save()
page.revisions.update(submitted_for_moderation=False)
# Clear approved_go_live_at for older revisions
page.revisions.update(
submitted_for_moderation=False,
approved_go_live_at=None,
)
else:
# not publishing the page
if page.live:
@ -229,7 +317,11 @@ def edit(request, page_id):
page.has_unpublished_changes = True
form.save()
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
if is_publishing:
messages.success(request, _("Page '{0}' published.").format(page.title))
@ -247,6 +339,7 @@ def edit(request, page_id):
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
messages.error(request, _("The page could not be saved due to validation errors"))
edit_handler = edit_handler_class(instance=page, form=form)
errors_debug = (
repr(edit_handler.form.errors)
@ -422,6 +515,12 @@ def preview(request):
"""
return render(request, 'wagtailadmin/pages/preview.html')
def preview_loading(request):
"""
This page is blank, but must be real HTML so its DOM can be written to once the preview of the page has rendered
"""
return HttpResponse("<html><head><title></title></head><body></body></html>")
@permission_required('wagtailadmin.access_admin')
def unpublish(request, page_id):
page = get_object_or_404(Page, id=page_id)
@ -432,6 +531,8 @@ def unpublish(request, page_id):
parent_id = page.get_parent().id
page.live = False
page.save()
# Since page is unpublished clear the approved_go_live_at of all revisions
page.revisions.update(approved_go_live_at=None)
messages.success(request, _("Page '{0}' unpublished.").format(page.title))
return redirect('wagtailadmin_explore', parent_id)
@ -534,7 +635,8 @@ def get_page_edit_handler(page_class):
if page_class not in PAGE_EDIT_HANDLERS:
PAGE_EDIT_HANDLERS[page_class] = TabbedInterface([
ObjectList(page_class.content_panels, heading='Content'),
ObjectList(page_class.promote_panels, heading='Promote')
ObjectList(page_class.promote_panels, heading='Promote'),
ObjectList(page_class.settings_panels, heading='Settings', classname="settings")
])
return PAGE_EDIT_HANDLERS[page_class]

View file

@ -0,0 +1,109 @@
import datetime
import json
from optparse import make_option
from django.core.management.base import BaseCommand
from django.utils import dateparse, timezone
from wagtail.wagtailcore.models import Page, PageRevision
def revision_date_expired(r):
expiry_str = json.loads(r.content_json).get('expire_at')
if not expiry_str:
return False
expire_at = dateparse.parse_datetime(expiry_str)
if expire_at < timezone.now():
return True
else:
return False
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option(
'--dryrun',
action='store_true',
dest='dryrun',
default=False,
help='Dry run -- don\'t change anything.'),
)
def handle(self, *args, **options):
dryrun = False
if options['dryrun']:
print "Will do a dry run."
dryrun = True
# 1. get all expired pages with live = True
expired_pages = Page.objects.filter(
live=True,
expire_at__lt=timezone.now()
)
if dryrun:
if expired_pages:
print "Expired pages to be deactivated:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
for ep in expired_pages:
print "{0}\t{1}\t{2}".format(
ep.expire_at.strftime("%Y-%m-%d %H:%M"),
ep.slug,
ep.title
)
else:
print "No expired pages to be deactivated found."
else:
expired_pages.update(expired=True, live=False)
# 2. get all page revisions for moderation that have been expired
expired_revs = [
r for r in PageRevision.objects.filter(
submitted_for_moderation=True
) if revision_date_expired(r)
]
if dryrun:
print "---------------------------------"
if expired_revs:
print "Expired revisions to be dropped from moderation queue:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
for er in expired_revs:
rev_data = json.loads(er.content_json)
print "{0}\t{1}\t{2}".format(
dateparse.parse_datetime(
rev_data.get('expire_at')
).strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
else:
print "No expired revision to be dropped from moderation."
else:
for er in expired_revs:
er.submitted_for_moderation = False
er.save()
# 3. get all revisions that need to be published
revs_for_publishing = PageRevision.objects.filter(
approved_go_live_at__lt=timezone.now()
)
if dryrun:
print "---------------------------------"
if revs_for_publishing:
print "Revisions to be published:"
print "Go live datetime\t\tSlug\t\tName"
print "---------------\t\t\t----\t\t----"
for rp in revs_for_publishing:
rev_data = json.loads(rp.content_json)
print "{0}\t\t{1}\t{2}".format(
rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
else:
print "No pages to go live."
else:
for rp in revs_for_publishing:
# just run publish for the revision -- since the approved go
# live datetime is before now it will make the page live
rp.publish()

View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'Site', fields ['hostname']
db.delete_unique(u'wagtailcore_site', ['hostname'])
# Adding unique constraint on 'Site', fields ['hostname', 'port']
db.create_unique(u'wagtailcore_site', ['hostname', 'port'])
def backwards(self, orm):
# Removing unique constraint on 'Site', fields ['hostname', 'port']
db.delete_unique(u'wagtailcore_site', ['hostname', 'port'])
# Adding unique constraint on 'Site', fields ['hostname']
db.create_unique(u'wagtailcore_site', ['hostname'])
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'PageRevision.approved_go_live_at'
db.add_column(u'wagtailcore_pagerevision', 'approved_go_live_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.go_live_at'
db.add_column(u'wagtailcore_page', 'go_live_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.expire_at'
db.add_column(u'wagtailcore_page', 'expire_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.expired'
db.add_column(u'wagtailcore_page', 'expired',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'PageRevision.approved_go_live_at'
db.delete_column(u'wagtailcore_pagerevision', 'approved_go_live_at')
# Deleting field 'Page.go_live_at'
db.delete_column(u'wagtailcore_page', 'go_live_at')
# Deleting field 'Page.expire_at'
db.delete_column(u'wagtailcore_page', 'expire_at')
# Deleting field 'Page.expired'
db.delete_column(u'wagtailcore_page', 'expired')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'expire_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'approved_go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -14,7 +14,11 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group
from django.conf import settings
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.utils.functional import cached_property
from treebeard.mp_tree import MP_Node
@ -25,29 +29,47 @@ from wagtail.wagtailsearch import Indexed, get_search_backend
class SiteManager(models.Manager):
def get_by_natural_key(self, hostname):
return self.get(hostname=hostname)
def get_by_natural_key(self, hostname, port):
return self.get(hostname=hostname, port=port)
class Site(models.Model):
hostname = models.CharField(max_length=255, unique=True, db_index=True)
hostname = models.CharField(max_length=255, db_index=True)
port = models.IntegerField(default=80, help_text=_("Set this to something other than 80 if you need a specific port number to appear in URLs (e.g. development on port 8000). Does not affect request handling (so port forwarding still works)."))
root_page = models.ForeignKey('Page', related_name='sites_rooted_here')
is_default_site = models.BooleanField(default=False, help_text=_("If true, this site will handle requests for all other hostnames that do not have a site entry of their own"))
class Meta:
unique_together = ('hostname', 'port')
def natural_key(self):
return (self.hostname,)
return (self.hostname, self.port)
def __unicode__(self):
return self.hostname + ("" if self.port == 80 else (":%d" % self.port)) + (" [default]" if self.is_default_site else "")
@staticmethod
def find_for_request(request):
"""Find the site object responsible for responding to this HTTP request object"""
"""
Find the site object responsible for responding to this HTTP
request object. Try:
- unique hostname first
- then hostname and port
- if there is no matching hostname at all, or no matching
hostname:port combination, fall back to the unique default site,
or raise an exception
NB this means that high-numbered ports on an extant hostname may
still be routed to a different hostname which is set as the default
"""
try:
hostname = request.META['HTTP_HOST'].split(':')[0]
# find a Site matching this specific hostname
return Site.objects.get(hostname=hostname)
hostname = request.META['HTTP_HOST'].split(':')[0] # KeyError here goes to the final except clause
try:
# find a Site matching this specific hostname
return Site.objects.get(hostname=hostname) # Site.DoesNotExist here goes to the final except clause
except Site.MultipleObjectsReturned:
# as there were more than one, try matching by port too
port = request.META['SERVER_PORT'] # KeyError here goes to the final except clause
return Site.objects.get(hostname=hostname, port=int(port)) # Site.DoesNotExist here goes to the final except clause
except (Site.DoesNotExist, KeyError):
# If no matching site exists, or request does not specify an HTTP_HOST (which
# will often be the case for the Django test client), look for a catch-all Site.
@ -63,6 +85,24 @@ class Site(models.Model):
else:
return 'http://%s:%d' % (self.hostname, self.port)
def clean_fields(self, exclude=None):
super(Site, self).clean_fields(exclude)
# Only one site can have the is_default_site flag set
try:
default = Site.objects.get(is_default_site=True)
except Site.DoesNotExist:
pass
except Site.MultipleObjectsReturned:
raise
else:
if self.is_default_site and self.pk != default.pk:
raise ValidationError(
{'is_default_site': [
_("%(hostname)s is already configured as the default site. You must unset that before you can save this site as default.")
% { 'hostname': default.hostname }
]}
)
# clear the wagtail_site_root_paths cache whenever Site records are updated
def save(self, *args, **kwargs):
result = super(Site, self).save(*args, **kwargs)
@ -135,6 +175,12 @@ class PageManager(models.Manager):
def not_live(self):
return self.get_queryset().not_live()
def in_menu(self):
return self.get_queryset().in_menu()
def not_in_menu(self):
return self.get_queryset().not_in_menu()
def page(self, other):
return self.get_queryset().page(other)
@ -226,6 +272,10 @@ class Page(MP_Node, ClusterableModel, Indexed):
show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus"))
search_description = models.TextField(blank=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expired = models.BooleanField(default=False, editable=False)
indexed_fields = {
'title': {
'type': 'string',
@ -320,10 +370,10 @@ class Page(MP_Node, ClusterableModel, Indexed):
SET url_path = %s || substring(url_path from %s)
WHERE path LIKE %s AND id <> %s
"""
cursor.execute(update_statement,
cursor.execute(update_statement,
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
@property
@cached_property
def specific(self):
"""
Return this page in its most specific subclassed form.
@ -337,7 +387,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
else:
return content_type.get_object_for_this_type(id=self.id)
@property
@cached_property
def specific_class(self):
"""
return the class that this page would be if instantiated in its
@ -366,24 +416,24 @@ class Page(MP_Node, ClusterableModel, Indexed):
else:
raise Http404
def save_revision(self, user=None, submitted_for_moderation=False):
self.revisions.create(content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation)
def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None):
return self.revisions.create(
content_json=self.to_json(),
user=user,
submitted_for_moderation=submitted_for_moderation,
approved_go_live_at=approved_go_live_at,
)
def get_latest_revision(self):
try:
revision = self.revisions.order_by('-created_at')[0]
except IndexError:
return False
return revision
return self.revisions.order_by('-created_at').first()
def get_latest_revision_as_page(self):
try:
revision = self.revisions.order_by('-created_at')[0]
except IndexError:
return self.specific
latest_revision = self.get_latest_revision()
return revision.as_page_object()
if latest_revision:
return latest_revision.as_page_object()
else:
return self.specific
def get_context(self, request, *args, **kwargs):
return {
@ -399,8 +449,8 @@ class Page(MP_Node, ClusterableModel, Indexed):
def serve(self, request, *args, **kwargs):
return TemplateResponse(
request,
self.get_template(request, *args, **kwargs),
request,
self.get_template(request, *args, **kwargs),
self.get_context(request, *args, **kwargs)
)
@ -413,6 +463,10 @@ class Page(MP_Node, ClusterableModel, Indexed):
return (not self.is_leaf()) or self.depth == 2
def get_other_siblings(self):
warnings.warn(
"The 'Page.get_other_siblings()' method has been replaced. "
"Use 'Page.get_siblings(inclusive=False)' instead.", DeprecationWarning)
# get sibling pages excluding self
return self.get_siblings().exclude(id=self.id)
@ -527,13 +581,22 @@ class Page(MP_Node, ClusterableModel, Indexed):
@property
def status_string(self):
if not self.live:
return "draft"
if self.expired:
return "expired"
elif self.approved_schedule:
return "scheduled"
else:
return "draft"
else:
if self.has_unpublished_changes:
return "live + draft"
else:
return "live"
@property
def approved_schedule(self):
return self.revisions.exclude(approved_go_live_at__isnull=True).exists()
def has_unpublished_subtree(self):
"""
An awkwardly-defined flag used in determining whether unprivileged editors have
@ -645,6 +708,12 @@ class Page(MP_Node, ClusterableModel, Indexed):
def get_siblings(self, inclusive=True):
return Page.objects.sibling_of(self, inclusive)
def get_next_siblings(self, inclusive=False):
return self.get_siblings(inclusive).filter(path__gte=self.path).order_by('path')
def get_prev_siblings(self, inclusive=False):
return self.get_siblings(inclusive).filter(path__lte=self.path).order_by('-path')
def get_navigation_menu_items():
# Get all pages that appear in the navigation menu: ones which have children,
@ -707,6 +776,7 @@ class PageRevision(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
content_json = models.TextField()
approved_go_live_at = models.DateTimeField(null=True, blank=True)
objects = models.Manager()
submitted_revisions = SubmittedRevisionsManager()
@ -740,11 +810,27 @@ class PageRevision(models.Model):
def publish(self):
page = self.as_page_object()
page.live = True
if page.go_live_at and page.go_live_at > timezone.now():
# if we have a go_live in the future don't make the page live
page.live = False
# Instead set the approved_go_live_at of this revision
self.approved_go_live_at = page.go_live_at
self.save()
# And clear the the approved_go_live_at of any other revisions
page.revisions.exclude(id=self.id).update(approved_go_live_at=None)
else:
page.live = True
# If page goes live clear the approved_go_live_at of all revisions
page.revisions.update(approved_go_live_at=None)
page.expired = False # When a page is published it can't be expired
page.save()
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)
def __unicode__(self):
return '"' + unicode(self.page) + '" at ' + unicode(self.created_at)
PAGE_PERMISSION_TYPE_CHOICES = [
('add', 'Add'),
('edit', 'Edit'),
@ -803,18 +889,39 @@ class UserPagePermissionsProxy(object):
if self.user.is_superuser:
return Page.objects.all()
editable_pages = Page.objects.none()
for perm in self.permissions.filter(permission_type='add'):
# user has edit permission on any subpage of perm.page
# (including perm.page itself) that is owned by them
editable_pages |= Page.objects.descendant_of(perm.page, inclusive=True).filter(owner=self.user)
for perm in self.permissions.filter(permission_type='edit'):
# user has edit permission on any subpage of perm.page
# (including perm.page itself) regardless of owner
editable_pages |= Page.objects.descendant_of(perm.page, inclusive=True)
return editable_pages
def can_edit_pages(self):
"""Return True if the user has permission to edit any pages"""
return True if self.editable_pages().count() else False
def publishable_pages(self):
"""Return a queryset of the pages that this user has permission to publish"""
# Deal with the trivial cases first...
if not self.user.is_active:
return Page.objects.none()
if self.user.is_superuser:
return Page.objects.all()
# Translate each of the user's permission rules into a Q-expression
q_expressions = []
for perm in self.permissions:
if perm.permission_type == 'add':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) that is owned by them
q_expressions.append(
Q(path__startswith=perm.page.path, owner=self.user)
)
elif perm.permission_type == 'edit':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) regardless of owner
if perm.permission_type == 'publish':
# user has publish permission on any subpage of perm.page
# (including perm.page itself)
q_expressions.append(
Q(path__startswith=perm.page.path)
)
@ -827,6 +934,11 @@ class UserPagePermissionsProxy(object):
else:
return Page.objects.none()
def can_publish_pages(self):
"""Return True if the user has permission to publish any pages"""
return True if self.publishable_pages().count() else False
class PagePermissionTester(object):
def __init__(self, user_perms, page):
self.user = user_perms.user

View file

@ -16,6 +16,15 @@ class PageQuerySet(MP_NodeQuerySet):
def not_live(self):
return self.exclude(self.live_q())
def in_menu_q(self):
return Q(show_in_menus=True)
def in_menu(self):
return self.filter(self.in_menu_q())
def not_in_menu(self):
return self.exclude(self.in_menu_q())
def page_q(self, other):
return Q(id=other.id)

View file

@ -1,24 +1,8 @@
from django import template
import warnings
from wagtail.wagtailcore.models import Page
register = template.Library()
warnings.warn(
"The pageurl tag library has been moved to wagtailcore_tags. "
"Use {% load wagtailcore_tags %} instead.", DeprecationWarning)
@register.simple_tag(takes_context=True)
def pageurl(context, page):
"""
Outputs a page's URL as relative (/foo/bar/) if it's within the same site as the
current page, or absolute (http://example.com/foo/bar/) if not.
"""
return page.relative_url(context['request'].site)
@register.simple_tag(takes_context=True)
def slugurl(context, slug):
"""Returns the URL for the page that has the given slug."""
page = Page.objects.filter(slug=slug).first()
if page:
return page.relative_url(context['request'].site)
else:
return None
from wagtail.wagtailcore.templatetags.wagtailcore_tags import register, pageurl

View file

@ -1,11 +1,8 @@
from django import template
from django.utils.safestring import mark_safe
import warnings
from wagtail.wagtailcore.rich_text import expand_db_html
register = template.Library()
warnings.warn(
"The rich_text tag library has been moved to wagtailcore_tags. "
"Use {% load wagtailcore_tags %} instead.", DeprecationWarning)
@register.filter
def richtext(value):
return mark_safe('<div class="rich-text">' + expand_db_html(value) + '</div>')
from wagtail.wagtailcore.templatetags.wagtailcore_tags import register, richtext

View file

@ -0,0 +1,32 @@
from django import template
from django.utils.safestring import mark_safe
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.rich_text import expand_db_html
register = template.Library()
@register.simple_tag(takes_context=True)
def pageurl(context, page):
"""
Outputs a page's URL as relative (/foo/bar/) if it's within the same site as the
current page, or absolute (http://example.com/foo/bar/) if not.
"""
return page.relative_url(context['request'].site)
@register.simple_tag(takes_context=True)
def slugurl(context, slug):
"""Returns the URL for the page that has the given slug."""
page = Page.objects.filter(slug=slug).first()
if page:
return page.relative_url(context['request'].site)
else:
return None
@register.filter
def richtext(value):
return mark_safe('<div class="rich-text">' + expand_db_html(value) + '</div>')

View file

@ -1,11 +1,13 @@
from StringIO import StringIO
from datetime import timedelta
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from django.contrib.auth.models import User
from django.utils import timezone
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.wagtailcore.models import Page, PageRevision, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
@ -87,3 +89,107 @@ class TestReplaceTextCommand(TestCase):
# Check that its now about easter
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")
class TestPublishScheduledPagesCommand(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
def test_go_live_page_will_be_published(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
go_live_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
def test_future_go_live_page_will_not_be_published(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
go_live_at=timezone.now() + timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
def test_expired_page_will_be_unpublished(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=True,
expire_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(p.expired)
def test_future_expired_page_will_not_be_unpublished(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=True,
expire_at=timezone.now() + timedelta(days=1),
)
self.root_page.add_child(instance=page)
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertFalse(p.expired)
def test_expired_pages_are_dropped_from_mod_queue(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
expire_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(submitted_for_moderation=True)
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())

View file

@ -9,30 +9,96 @@ from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
class TestRouting(TestCase):
class TestSiteRouting(TestCase):
fixtures = ['test.json']
def test_find_site_for_request(self):
default_site = Site.objects.get(is_default_site=True)
def setUp(self):
self.default_site = Site.objects.get(is_default_site=True)
events_page = Page.objects.get(url_path='/home/events/')
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
about_page = Page.objects.get(url_path='/home/about-us/')
self.events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
self.alternate_port_events_site = Site.objects.create(hostname='events.example.com', root_page=events_page, port='8765')
self.about_site = Site.objects.create(hostname='about.example.com', root_page=about_page)
self.unrecognised_port = '8000'
self.unrecognised_hostname = 'unknown.site.com'
def test_no_host_header_routes_to_default_site(self):
# requests without a Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
self.assertEqual(Site.find_for_request(request), default_site)
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_valid_headers_route_to_specific_site(self):
# requests with a known Host: header should be directed to the specific site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'events.example.com'
self.assertEqual(Site.find_for_request(request), events_site)
request.META['HTTP_HOST'] = self.events_site.hostname
request.META['SERVER_PORT'] = self.events_site.port
self.assertEqual(Site.find_for_request(request), self.events_site)
def test_ports_in_request_headers_are_respected(self):
# ports in the Host: header should be respected
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.alternate_port_events_site.hostname
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
def test_unrecognised_host_header_routes_to_default_site(self):
# requests with an unrecognised Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'unknown.example.com'
self.assertEqual(Site.find_for_request(request), default_site)
request.META['HTTP_HOST'] = self.unrecognised_hostname
request.META['SERVER_PORT'] = '80'
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_and_default_host_routes_to_default_site(self):
# requests to the default host on an unrecognised port should be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.default_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_and_unrecognised_host_routes_to_default_site(self):
# requests with an unrecognised Host: header _and_ an unrecognised port
# hould be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.unrecognised_hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_on_known_hostname_routes_there_if_no_ambiguity(self):
# requests on an unrecognised port should be directed to the site with
# matching hostname if there is no ambiguity
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.about_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.about_site)
def test_unrecognised_port_on_known_hostname_routes_to_default_site_if_ambiguity(self):
# requests on an unrecognised port should be directed to the default
# site, even if their hostname (but not port) matches more than one
# other entry
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.events_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_port_in_http_host_header_is_ignored(self):
# port in the HTTP_HOST header is ignored
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = "%s:%s" % (self.events_site.hostname, self.events_site.port)
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
class TestRouting(TestCase):
fixtures = ['test.json']
def test_urls(self):
default_site = Site.objects.get(is_default_site=True)
@ -224,3 +290,24 @@ class TestMovePage(TestCase):
christmas = events_index.get_children().get(slug='christmas')
self.assertEqual(christmas.depth, 5)
self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/')
class TestPrevNextSiblings(TestCase):
fixtures = ['test.json']
def test_get_next_siblings(self):
christmas_event = Page.objects.get(url_path='/home/events/christmas/')
self.assertTrue(christmas_event.get_next_siblings().filter(url_path='/home/events/final-event/').exists())
def test_get_next_siblings_inclusive(self):
christmas_event = Page.objects.get(url_path='/home/events/christmas/')
# First element must always be the current page
self.assertEqual(christmas_event.get_next_siblings(inclusive=True).first(), christmas_event)
def test_get_prev_siblings(self):
final_event = Page.objects.get(url_path='/home/events/final-event/')
self.assertTrue(final_event.get_prev_siblings().filter(url_path='/home/events/christmas/').exists())
# First element must always be the current page
self.assertEqual(final_event.get_prev_siblings(inclusive=True).first(), final_event)

View file

@ -176,13 +176,26 @@ class TestPagePermission(TestCase):
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
user_perms = UserPagePermissionsProxy(event_editor)
editable_pages = user_perms.editable_pages()
can_edit_pages = user_perms.can_edit_pages()
publishable_pages = user_perms.publishable_pages()
can_publish_pages = user_perms.can_publish_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertTrue(can_edit_pages)
self.assertFalse(publishable_pages.filter(id=homepage.id).exists())
self.assertFalse(publishable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(publishable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_publish_pages)
def test_editable_pages_for_user_with_edit_permission(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
@ -190,13 +203,26 @@ class TestPagePermission(TestCase):
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
user_perms = UserPagePermissionsProxy(event_moderator)
editable_pages = user_perms.editable_pages()
can_edit_pages = user_perms.can_edit_pages()
publishable_pages = user_perms.publishable_pages()
can_publish_pages = user_perms.can_publish_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertTrue(can_edit_pages)
self.assertFalse(publishable_pages.filter(id=homepage.id).exists())
self.assertTrue(publishable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(publishable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertTrue(can_publish_pages)
def test_editable_pages_for_inactive_user(self):
user = User.objects.get(username='inactiveuser')
homepage = Page.objects.get(url_path='/home/')
@ -204,13 +230,26 @@ class TestPagePermission(TestCase):
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
user_perms = UserPagePermissionsProxy(user)
editable_pages = user_perms.editable_pages()
can_edit_pages = user_perms.can_edit_pages()
publishable_pages = user_perms.publishable_pages()
can_publish_pages = user_perms.can_publish_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_edit_pages)
self.assertFalse(publishable_pages.filter(id=homepage.id).exists())
self.assertFalse(publishable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(publishable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_publish_pages)
def test_editable_pages_for_superuser(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
@ -218,9 +257,49 @@ class TestPagePermission(TestCase):
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
user_perms = UserPagePermissionsProxy(user)
editable_pages = user_perms.editable_pages()
can_edit_pages = user_perms.can_edit_pages()
publishable_pages = user_perms.publishable_pages()
can_publish_pages = user_perms.can_publish_pages()
self.assertTrue(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertTrue(can_edit_pages)
self.assertTrue(publishable_pages.filter(id=homepage.id).exists())
self.assertTrue(publishable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(publishable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertTrue(can_publish_pages)
def test_editable_pages_for_non_editing_user(self):
user = User.objects.get(username='admin_only_user')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
user_perms = UserPagePermissionsProxy(user)
editable_pages = user_perms.editable_pages()
can_edit_pages = user_perms.can_edit_pages()
publishable_pages = user_perms.publishable_pages()
can_publish_pages = user_perms.can_publish_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_edit_pages)
self.assertFalse(publishable_pages.filter(id=homepage.id).exists())
self.assertFalse(publishable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(publishable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_publish_pages)

View file

@ -34,6 +34,27 @@ class TestPageQuerySet(TestCase):
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_in_menu(self):
pages = Page.objects.in_menu()
# All pages must be be in the menus
for page in pages:
self.assertTrue(page.show_in_menus)
# Check that the events index is in the results
events_index = Page.objects.get(url_path='/home/events/')
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_in_menu(self):
pages = Page.objects.not_in_menu()
# All pages must not be in menus
for page in pages:
self.assertFalse(page.show_in_menus)
# Check that the root page is in the results
self.assertTrue(pages.filter(id=1).exists())
def test_page(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.page(homepage)

View file

@ -0,0 +1,262 @@
from mock import patch
from django.test import TestCase
from wagtail.wagtailcore.rich_text import (
ImageEmbedHandler,
MediaEmbedHandler,
PageLinkHandler,
DocumentLinkHandler,
DbWhitelister,
extract_attrs,
expand_db_html
)
from bs4 import BeautifulSoup
class TestImageEmbedHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-id="test-id" data-format="test-format" data-alt="test-alt">foo</b>'
)
tag = soup.b
result = ImageEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'alt': 'test-alt',
'id': 'test-id',
'format': 'test-format'})
def test_expand_db_attributes_page_does_not_exist(self):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<img>')
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
False
)
self.assertIn('<img class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
True
)
self.assertIn('<img data-embedtype="image" data-id="1" data-format="left" data-alt="test-alt" class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor_throws_exception(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'format': 'left'},
True
)
self.assertEqual(result, '')
class TestMediaEmbedHandler(TestCase):
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-url="test-url">foo</b>'
)
tag = soup.b
result = MediaEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'url': 'test-url'})
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
True
)
self.assertIn('<div class="embed-placeholder" contenteditable="false" data-embedtype="media" data-url="http://www.youtube.com/watch/">', result)
self.assertIn('<h3>test title</h3>', result)
self.assertIn('<p>URL: http://www.youtube.com/watch/</p>', result)
self.assertIn('<p>Provider: test provider name</p>', result)
self.assertIn('<p>Author: test author name</p>', result)
self.assertIn('<img src="test thumbnail url" alt="test title">', result)
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_not_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
False
)
self.assertIn('test html', result)
class TestPageLinkHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<a data-id="test-id">foo</a>'
)
tag = soup.a
result = PageLinkHandler.get_db_attributes(tag)
self.assertEqual(result,
{'id': 'test-id'})
def test_expand_db_attributes_page_does_not_exist(self):
result = PageLinkHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<a>')
def test_expand_db_attributes_for_editor(self):
result = PageLinkHandler.expand_db_attributes(
{'id': 1},
True
)
self.assertEqual(result,
'<a data-linktype="page" data-id="1" href="None">')
def test_expand_db_attributes_not_for_editor(self):
result = PageLinkHandler.expand_db_attributes(
{'id': 1},
False
)
self.assertEqual(result, '<a href="None">')
class TestDocumentLinkHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<a data-id="test-id">foo</a>'
)
tag = soup.a
result = DocumentLinkHandler.get_db_attributes(tag)
self.assertEqual(result,
{'id': 'test-id'})
def test_expand_db_attributes_document_does_not_exist(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<a>')
def test_expand_db_attributes_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
True
)
self.assertEqual(result,
'<a data-linktype="document" data-id="1" href="/documents/1/">')
def test_expand_db_attributes_not_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
False
)
self.assertEqual(result,
'<a href="/documents/1/">')
class TestDbWhiteLister(TestCase):
def test_clean_tag_node_div(self):
soup = BeautifulSoup(
'<div>foo</div>'
)
tag = soup.div
self.assertEqual(tag.name, 'div')
DbWhitelister.clean_tag_node(soup, tag)
self.assertEqual(tag.name, 'p')
def test_clean_tag_node_with_data_embedtype(self):
soup = BeautifulSoup(
'<p><a data-embedtype="image" data-id=1 data-format="left" data-alt="bar" irrelevant="baz">foo</a></p>'
)
tag = soup.p
DbWhitelister.clean_tag_node(soup, tag)
self.assertEqual(str(tag),
'<p><embed alt="bar" embedtype="image" format="left" id="1"/></p>')
def test_clean_tag_node_with_data_linktype(self):
soup = BeautifulSoup(
'<a data-linktype="document" data-id="1" irrelevant="baz">foo</a>'
)
tag = soup.a
DbWhitelister.clean_tag_node(soup, tag)
self.assertEqual(str(tag), '<a id="1" linktype="document">foo</a>')
def test_clean_tag_node(self):
soup = BeautifulSoup(
'<a irrelevant="baz">foo</a>'
)
tag = soup.a
DbWhitelister.clean_tag_node(soup, tag)
self.assertEqual(str(tag), '<a>foo</a>')
class TestExtractAttrs(TestCase):
def test_extract_attr(self):
html = '<a foo="bar" baz="quux">snowman</a>'
result = extract_attrs(html)
self.assertEqual(result, {'foo': 'bar', 'baz': 'quux'})
class TestExpandDbHtml(TestCase):
def test_expand_db_html_with_linktype(self):
html = '<a id="1" linktype="document">foo</a>'
result = expand_db_html(html)
self.assertEqual(result, '<a>foo</a>')
def test_expand_db_html_no_linktype(self):
html = '<a id="1">foo</a>'
result = expand_db_html(html)
self.assertEqual(result, '<a id="1">foo</a>')
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_html_with_embed(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
html = '<embed embedtype="media" url="http://www.youtube.com/watch" />'
result = expand_db_html(html)
self.assertIn('test html', result)

View file

@ -1,12 +1,7 @@
from StringIO import StringIO
from django.test import TestCase
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
from wagtail.wagtailcore.models import Page, Site
from wagtail.tests.models import SimplePage
class TestPageUrlTags(TestCase):
@ -15,22 +10,25 @@ class TestPageUrlTags(TestCase):
def test_pageurl_tag(self):
response = self.client.get('/events/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
self.assertContains(response,
'<a href="/events/christmas/">Christmas</a>')
def test_slugurl_tag(self):
response = self.client.get('/events/christmas/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/">Back to events index</a>')
self.assertContains(response,
'<a href="/events/">Back to events index</a>')
class TestIssue7(TestCase):
"""
This tests for an issue where if a site root page was moved, all the page
urls in that site would change to None.
This tests for an issue where if a site root page was moved, all
the page urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was moved. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
The issue was caused by the 'wagtail_site_root_paths' cache
variable not being cleared when a site root page was moved. Which
left all the child pages thinking that they are no longer in the
site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/7
@ -67,12 +65,13 @@ class TestIssue7(TestCase):
class TestIssue157(TestCase):
"""
This tests for an issue where if a site root pages slug was changed, all the page
urls in that site would change to None.
This tests for an issue where if a site root pages slug was
changed, all the page urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was changed. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
The issue was caused by the 'wagtail_site_root_paths' cache
variable not being cleared when a site root page was changed.
Which left all the child pages thinking that they are no longer in
the site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/157

View file

@ -1,6 +1,6 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% load image_tags %}
{% load wagtailimages_tags %}
{% block titletag %}{% trans "Add a document" %}{% endblock %}
{% block bodyclass %}menu-documents{% endblock %}
{% block extra_css %}

View file

@ -1,6 +1,6 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% load image_tags %}
{% load wagtailimages_tags %}
{% block titletag %}{% blocktrans with title=document.title %}Editing {{ title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-documents{% endblock %}
{% block extra_css %}

View file

@ -162,6 +162,10 @@ def get_embed(url, max_width=None, finder=None):
except (TypeError, ValueError):
embed_dict['height'] = None
# Make sure html field is valid
if 'html' not in embed_dict or not embed_dict['html']:
embed_dict['html'] = ''
# Create database record
embed, created = Embed.objects.get_or_create(
url=url,

View file

@ -1,6 +1,5 @@
from __future__ import division # Use true division
from django.utils.html import escape
from django.template.loader import render_to_string
from wagtail.wagtailembeds import get_embed

View file

@ -1,4 +1,4 @@
{% load image_tags %}
{% load wagtailimages_tags %}
{% load i18n %}
{% trans "Insert embed" as ins_emb_str %}
{% include "wagtailadmin/shared/header.html" with title=ins_emb_str merged=1 %}

View file

@ -1,24 +1,8 @@
from django import template
from django.utils.safestring import mark_safe
import warnings
from wagtail.wagtailembeds import get_embed
warnings.warn(
"The embed_filters tag library has been moved to wagtailembeds_tags. "
"Use {% load wagtailembeds_tags %} instead.", DeprecationWarning)
register = template.Library()
@register.filter
def embed(url, max_width=None):
embed = get_embed(url, max_width=max_width)
try:
if embed is not None:
return mark_safe(embed.html)
else:
return ''
except:
return ''
@register.filter
def embedly(url, max_width=None):
return embed(url, max_width)
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register, embed, embedly

View file

@ -0,0 +1,30 @@
import warnings
from django import template
from django.utils.safestring import mark_safe
from wagtail.wagtailembeds import get_embed
register = template.Library()
@register.filter
def embed(url, max_width=None):
embed = get_embed(url, max_width=max_width)
try:
if embed is not None:
return mark_safe(embed.html)
else:
return ''
except:
return ''
@register.filter
def embedly(url, max_width=None):
warnings.warn(
"The 'embedly' filter has been renamed. "
"Use 'embed' instead.", DeprecationWarning)
return embed(url, max_width)

View file

@ -7,6 +7,7 @@ try:
except ImportError:
no_embedly = True
from django import template
from django.test import TestCase
from wagtail.tests.utils import WagtailTestUtils, unittest
@ -18,7 +19,7 @@ from wagtail.wagtailembeds.embeds import (
AccessDeniedEmbedlyException,
)
from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly, oembed as wagtail_oembed
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter, embedly as embedly_filter
class TestEmbeds(TestCase):
@ -79,6 +80,19 @@ class TestEmbeds(TestCase):
# Width must be set to None
self.assertEqual(embed.width, None)
def test_no_html(self) :
def no_html_finder(url, max_width=None):
"""
A finder which returns everything but HTML
"""
embed = self.dummy_finder(url, max_width)
embed['html'] = None
return embed
embed = get_embed('www.test.com/1234', max_width=400, finder=no_html_finder)
self.assertEqual(embed.html, '')
class TestChooser(TestCase, WagtailTestUtils):
def setUp(self):
@ -245,3 +259,42 @@ class TestOembed(TestCase):
'height': 'test_height',
'html': 'test_html'
})
class TestEmbedFilter(TestCase):
def setUp(self):
class DummyResponse(object):
def read(self):
return "foo"
self.dummy_response = DummyResponse()
@patch('urllib2.urlopen')
@patch('json.loads')
def test_valid_embed(self, loads, urlopen):
urlopen.return_value = self.dummy_response
loads.return_value = {'type': 'photo',
'url': 'http://www.example.com'}
result = embed_filter('http://www.youtube.com/watch/')
self.assertEqual(result, '<img src="http://www.example.com" />')
@patch('urllib2.urlopen')
@patch('json.loads')
def test_render_filter(self, loads, urlopen):
urlopen.return_value = self.dummy_response
loads.return_value = {'type': 'photo',
'url': 'http://www.example.com'}
temp = template.Template('{% load wagtailembeds_tags %}{{ "http://www.youtube.com/watch/"|embed }}')
context = template.Context()
result = temp.render(context)
self.assertEqual(result, '<img src="http://www.example.com" />')
@patch('urllib2.urlopen')
@patch('json.loads')
def test_render_filter_nonexistent_type(self, loads, urlopen):
urlopen.return_value = self.dummy_response
loads.return_value = {'type': 'foo',
'url': 'http://www.example.com'}
temp = template.Template('{% load wagtailembeds_tags %}{{ "http://www.youtube.com/watch/"|embed }}')
context = template.Context()
result = temp.render(context)
self.assertEqual(result, '')

View file

@ -8,22 +8,9 @@ class BaseForm(django.forms.Form):
return super(BaseForm, self).__init__(*args, **kwargs)
class FormBuilder():
formfields = SortedDict()
class FormBuilder(object):
def __init__(self, fields):
for field in fields:
options = self.get_options(field)
f = getattr(self, "create_"+field.field_type+"_field")(field, options)
self.formfields[field.clean_name] = f
def get_options(self, field):
options = {}
options['label'] = field.label
options['help_text'] = field.help_text
options['required'] = field.required
options['initial'] = field.default_value
return options
self.fields = fields
def create_singleline_field(self, field, options):
# TODO: This is a default value - it may need to be changed
@ -72,16 +59,52 @@ class FormBuilder():
def create_checkbox_field(self, field, options):
return django.forms.BooleanField(**options)
FIELD_TYPES = {
'singleline': create_singleline_field,
'multiline': create_multiline_field,
'date': create_date_field,
'datetime': create_datetime_field,
'email': create_email_field,
'url': create_url_field,
'number': create_number_field,
'dropdown': create_dropdown_field,
'radio': create_radio_field,
'checkboxes': create_checkboxes_field,
'checkbox': create_checkbox_field,
}
@property
def formfields(self):
formfields = SortedDict()
for field in self.fields:
options = self.get_field_options(field)
if field.field_type in self.FIELD_TYPES:
formfields[field.clean_name] = self.FIELD_TYPES[field.field_type](self, field, options)
else:
raise Exception("Unrecognised field type: " + form.field_type)
return formfields
def get_field_options(self, field):
options = {}
options['label'] = field.label
options['help_text'] = field.help_text
options['required'] = field.required
options['initial'] = field.default_value
return options
def get_form_class(self):
return type('WagtailForm', (BaseForm,), self.formfields)
class SelectDateForm(django.forms.Form):
date_from = django.forms.DateField(
date_from = django.forms.DateTimeField(
required=False,
widget=django.forms.DateInput(attrs={'placeholder': 'Date from'})
)
date_to = django.forms.DateField(
date_to = django.forms.DateTimeField(
required=False,
widget=django.forms.DateInput(attrs={'placeholder': 'Date to'})
)

Some files were not shown because too many files have changed in this diff Show more