Merge remote-tracking branch 'torchbox/master' into cloudflare-cache-invalidation

This commit is contained in:
Karl Hobley 2014-10-08 12:08:32 +01:00
commit 51a30bfb54
140 changed files with 4485 additions and 1243 deletions

View file

@ -60,3 +60,9 @@ file_filter = wagtail/wagtailforms/locale/<lang>/LC_MESSAGES/django.po
source_file = wagtail/wagtailforms/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO
[wagtail.wagtailsites]
file_filter = wagtail/wagtailsites/locale/<lang>/LC_MESSAGES/django.po
source_file = wagtail/wagtailsites/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO

View file

@ -4,13 +4,25 @@ Changelog
0.7 (xx.xx.2014)
~~~~~~~~~~~~~~~~
* Added interface for choosing focal point on images
* Redesigned and reorganised navigation menu to include a 'Settings' submenu
* Added Groups administration area
* Added Sites administration area
* Added the ability to lock a page to (temporarily) prevent edits to that page
* Removed 'content_type' template filter from the project template, as the same thing can be accomplished with self.get_verbose_name|slugify
* Page copy operations now also copy the page revision history
* Page models now support a 'parent_page_types' property in addition to 'subpage types', to restrict the types of page they can be created under
* 'register_snippet' can now be invoked as a decorator
* Project template updated to Django 1.7
* 'boost' applied to the title field on searches reduced from 100 to 2
* The 'type' method of PageQuerySet (used to filter the queryset to a specific page type) now includes subclasses of the given page type.
* The 'update_index' management command now updates all backends listed in WAGTAILSEARCH_BACKENDS, or a specific one passed on the command line, rather than just the default backend
* The 'fill' image resize method now supports an additional parameter defining the closeness of the crop
* Fix: 'wagtail start' command now works on Windows
* Fix: The external image URL generator no longer stores generated images in Django's cache
* Fix: Elasticsearch backend can now search querysets that have been filtered with an 'in' clause of a non-list type (such as a ValuesListQuerySet)
* Fix: Logic around the has_unpublished_changes flag has been fixed, to prevent issues with the 'View draft' button failing to show in some cases
* Fix: It is now easier to move pages to the beginning and end of their section
* Fix: Image rendering no longer creates erroneous duplicate Rendition records when the focal point is blank.
0.6 (11.09.2014)
~~~~~~~~~~~~~~~~

View file

@ -1,6 +1,189 @@
======
Images
======
.. _image_tag:
Using images in templates
=========================
.. 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::
{% image [image] [resize-rule] %}
For example:
.. code-block:: django
{% load wagtailimages_tags %}
...
{% image self.photo width-400 %}
<!-- or a square thumbnail: -->
{% image self.photo fill-80x80 %}
In the above syntax example ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page; various resizing methods are supported, to cater for different usage cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).
Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces.
The available resizing methods are:
.. glossary::
``max``
(takes two dimensions)
.. code-block:: django
{% image self.photo max-1000x500 %}
Fit **within** the given dimensions.
The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250.
``min``
(takes two dimensions)
.. code-block:: django
{% image self.photo min-500x200 %}
**Cover** the given dimensions.
This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height.
``width``
(takes one dimension)
.. code-block:: django
{% image self.photo width-640 %}
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
.. code-block:: django
{% image self.photo height-480 %}
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions and an optional ``-c`` parameter)
.. code-block:: django
{% image self.photo fill-200x200 %}
Resize and **crop** to fill the **exact** dimensions.
This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.
This filter will crop to the image's focal point if it has been set. If not, it will crop to the centre of the image.
**Cropping closer to the focal point**
By default, Wagtail will only crop to change the aspect ratio of the image.
In some cases (thumbnails, for example) it may be nice to crop closer to the focal point so the subject of the image is easier to see.
You can do this by appending ``-c<percentage>`` at the end of the method. For example, if you would like the image to be cropped as closely as possible to its focal point, add ``-c100`` to the end of the method.
.. code-block:: django
{% image self.photo fill-200x200-c100 %}
This will crop the image as much as it an but will never crop into the focal point.
If you find that ``-c100`` is too close, you can try ``-c75`` or ``-c50`` (any whole number from 0 to 100 is accepted).
``original``
(takes no dimensions)
.. code-block:: django
{% image self.photo original %}
Leaves the image at its original size - no resizing is performed.
.. Note::
Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
.. _image_tag_alt:
More control over the ``img`` tag
---------------------------------
Wagtail provides two shortcuts to give greater control over the ``img`` element:
**1. Adding attributes to the {% image %} tag**
.. versionadded:: 0.4
Extra attributes can be specified with the syntax ``attribute="value"``:
.. code-block:: django
{% image self.photo width-400 class="foo" id="bar" %}
No validation is performed on attributes added in this way so it's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
**2. Generating the image "as foo" to access individual properties**
Wagtail can assign the image data to another variable using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
This syntax exposes the underlying image "Rendition" (``tmp_photo``) to the developer. A "Rendition" contains just the information specific to the way you've requested to format the image i.e dimensions and source URL.
If your site defines a custom image model using ``AbstractImage``, then any additional fields you add to an image e.g a copyright holder, are **not** part of the image *rendition*, they're part of the image *model*.
Therefore in the above example, if you'd added the field ``foo`` to your AbstractImage you'd access it using ``{{ self.photo.foo }}`` not ``{{ tmp_photo.foo }}``.
(Due to the links in the database between renditions and their parent image, you could also access it as ``{{ tmp_photo.image.foo }}`` but this is clearly confusing.)
.. Note::
The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``.
The ``attrs`` shortcut
-----------------------
.. versionadded:: 0.4
You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
.. code-block:: django
<img {{ tmp_photo.attrs }} class="my-custom-class" />
Advanced topics
===============
.. toctree::
:maxdepth: 2

View file

@ -3,6 +3,7 @@ Core components
.. toctree::
:maxdepth: 2
:titlesonly:
sites
pages/index

View file

@ -170,7 +170,7 @@ Reference
.. automethod:: search
See: :ref:`wagtailsearch_for_python_developers`
See: :ref:`wagtailsearch_searching_pages`
Example:
@ -178,3 +178,21 @@ Reference
# Search future events
results = EventPage.objects.live().filter(date__gt=timezone.now()).search("Hello")
.. automethod:: type
Example:
.. code-block:: python
# Find all pages that are of type AbstractEmailForm, or a descendant of it
form_pages = Page.objects.type(AbstractEmailForm)
.. automethod:: unpublish
Example:
.. code-block:: python
# Unpublish current_page and all of its children
Page.objects.descendant_of(current_page, inclusive=True).unpublish()

View file

@ -133,7 +133,7 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. attribute:: search_fields
A list of fields to be indexed by the search engine. See Search docs :ref:`wagtailsearch_for_python_developers`
A list of fields to be indexed by the search engine. See Search docs :ref:`wagtailsearch_indexing_fields`
.. attribute:: subpage_types

View file

@ -556,6 +556,13 @@ The available hooks are:
def register_frank_menu_item():
return MenuItem('Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000)
.. _register_settings_menu_item:
``register_settings_menu_item``
.. versionadded:: 0.7
As ``register_admin_menu_item``, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
.. _construct_main_menu:
``construct_main_menu``
@ -641,6 +648,13 @@ The available hooks are:
'a': attribute_rule({'href': check_url, 'target': True}),
}
.. _register_permissions:
``register_permissions``
.. versionadded:: 0.7
Return a queryset of Permission objects to be shown in the Groups administration area.
Image Formats in the Rich Text Editor
-------------------------------------

View file

@ -86,13 +86,9 @@ Template tags & filters
In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be ``load``-ed `as you would any other <https://docs.djangoproject.com/en/dev/topics/templates/#custom-tag-and-filter-libraries>`_
.. _image_tag:
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`.
@ -112,139 +108,8 @@ For example:
<!-- or a square thumbnail: -->
{% image self.photo fill-80x80 %}
In the above syntax example ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page; various resizing methods are supported, to cater for different usage cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).
Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces.
The available resizing methods are:
.. glossary::
``max``
(takes two dimensions)
.. code-block:: django
{% image self.photo max-1000x500 %}
Fit **within** the given dimensions.
The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250.
``min``
(takes two dimensions)
.. code-block:: django
{% image self.photo min-500x200 %}
**Cover** the given dimensions.
This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height.
``width``
(takes one dimension)
.. code-block:: django
{% image self.photo width-640 %}
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
.. code-block:: django
{% image self.photo height-480 %}
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions)
.. code-block:: django
{% image self.photo fill-200x200 %}
Resize and **crop** to fill the **exact** dimensions.
This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.
**The crop always aligns on the centre of the image.**
``original``
(takes no dimensions)
.. code-block:: django
{% image self.photo original %}
Leaves the image at its original size - no resizing is performed.
.. Note::
Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
.. _image_tag_alt:
More control over the ``img`` tag
---------------------------------
Wagtail provides two shorcuts to give greater control over the ``img`` element:
**1. Adding attributes to the {% image %} tag**
.. versionadded:: 0.4
Extra attributes can be specified with the syntax ``attribute="value"``:
.. code-block:: django
{% image self.photo width-400 class="foo" id="bar" %}
No validation is performed on attributes added in this way so it's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
**2. Generating the image "as foo" to access individual properties**
Wagtail can assign the image data to another variable using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
This syntax exposes the underlying image "Rendition" (``tmp_photo``) to the developer. A "Rendition" contains just the information specific to the way you've requested to format the image i.e dimensions and source URL.
If your site defines a custom image model using ``AbstractImage``, then any additional fields you add to an image e.g a copyright holder, are **not** part of the image *rendition*, they're part of the image *model*.
Therefore in the above example, if you'd added the field ``foo`` to your AbstractImage you'd access it using ``{{ self.photo.foo }}`` not ``{{ tmp_photo.foo }}``.
(Due to the links in the database between renditions and their parent image, you could also access it as ``{{ tmp_photo.image.foo }}`` but this is clearly confusing.)
.. Note::
The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``.
The ``attrs`` shortcut
-----------------------
.. versionadded:: 0.4
You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
.. code-block:: django
<img {{ tmp_photo.attrs }} class="my-custom-class" />
See :ref:`image_tag` for full documentation.
.. _rich-text-filter:

View file

@ -1,41 +0,0 @@
.. _editors-picks:
Editor's picks
==============
Editor's picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content in addition to results from the search algorithm. In a template using the search results view, editor's picks can be accessed through the variable ``query.editors_picks``. To include editor's picks in your search results template, use the following properties.
``query.editors_picks.all``
This gathers all of the editor's picks objects relating to the current query, in order according to their sort order in the Wagtail admin. You can then iterate through them using a ``{% for ... %}`` loop. Each editor's pick object provides these properties:
``editors_pick.page``
The page object associated with the pick. Use ``{% pageurl editors_pick.page %}`` to generate a URL or provide other properties of the page object.
``editors_pick.description``
The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms.
Putting this all together, a block of your search results template displaying editor's picks might look like this:
.. code-block:: django
{% with query.editors_picks.all as editors_picks %}
{% if editors_picks %}
<div class="well">
<h3>Editors picks</h3>
<ul>
{% for editors_pick in editors_picks %}
<li>
<h4>
<a href="{% pageurl editors_pick.page %}">
{{ editors_pick.page.title }}
</a>
</h4>
<p>{{ editors_pick.description|safe }}</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}

View file

@ -1,17 +1,43 @@
.. _wagtailsearch:
======
Search
======
Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through "Editor's Picks." Wagtail also collects simple statistics on queries made through the search interface.
Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through "Editor's Picks". Wagtail also collects simple statistics on queries made through the search interface.
.. toctree::
:maxdepth: 2
for_python_developers
frontend_views
editors_picks
indexing
searching
backends
Indexing
========
To make objects searchable, they firstly need to be added to the search index. This involves configuring the models/fields that you would like to index (this is done for you for Pages, Images and Documents) and then actually inserting them into the index.
See :ref:`wagtailsearch_indexing_update` for information on how to keep the objects in your search index in sync with the objects in your database.
If you have created some extra fields in a subclass of ``Page`` or ``Image``, you may want to add these new fields to the search index too so a users search query will match on their content. See :ref:`wagtailsearch_indexing_fields`.
If you have a custom model which doesn't derive from ``Page`` or ``Image`` that you would like to make searchable, see :ref:`wagtailsearch_indexing_models`.
Searching
=========
Wagtail provides an API for performing search queries on your models. You can also perform search queries on Django QuerySets.
See :ref:`wagtailsearch_searching`.
Backends
========
Wagtail provides two backends for storing the search index and performing search queries: Elasticsearch and the database. It's also possible to roll your own search backend.
See :ref:`wagtailsearch_backends`

View file

@ -1,34 +1,63 @@
.. _wagtailsearch_for_python_developers:
.. _wagtailsearch_indexing:
=====================
For Python developers
=====================
========
Indexing
========
To make a model searchable, you'll firstly need to add it into the search index. All pages, images and documents are indexed for you and you can start searching them right away.
If you have created some extra fields in a subclass of Page or Image, you may want to add these new fields to the search index too so that a user's search query will match on their content. See :ref:`wagtailsearch_indexing_fields` for info on how to do this.
If you have a custom model that you would like to make searchable, see :ref:`wagtailsearch_indexing_models`.
Basic usage
===========
By default using the :ref:`wagtailsearch_backends_database`, Wagtail's search will only index the ``title`` field of pages.
All searches are performed on Django QuerySets. Wagtail provides a ``search`` method on the queryset for all page models:
.. code-block:: python
# Search future EventPages
>>> from wagtail.wagtailcore.models import EventPage
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!")
.. _wagtailsearch_indexing_update:
All methods of ``PageQuerySet`` are supported by wagtailsearch:
Updating the index
==================
.. code-block:: python
If the search index is kept separate from the database (when using Elasticsearch for example), you need to keep them both in sync. There are two ways to do this: using the search signal handlers, or calling the ``update_index`` command periodically. For best speed and reliability, it's best to use both if possible.
# Search all live EventPages that are under the events index
>>> EventPage.objects.live().descendant_of(events_index).search("Event")
[<EventPage: Event 1>, <EventPage: Event 2>]
Signal handlers
---------------
Wagtailsearch provides some signal handlers which bind to the save/delete signals of all indexed models. This would automatically add and delete them from all backends you have registered in ``WAGTAILSEARCH_BACKENDS``.
To register the signal handlers, add the following code somewhere it would be executed at startup. We reccommend adding this to your projects ``urls.py``:
.. code-block: python
# urls.py
from wagtail.wagtailsearch.signal_handlers import register_signal_handlers
register_signal_handlers()
.. note::
If your project was made with the ``wagtail start`` command, this will already be set up for you.
The ``update_index`` command
----------------------------
Wagtail also provides a command for rebuilding the index from scratch.
:code:`./manage.py update_index`
It is recommended to run this command once a week and at the following times:
- whenever any pages have been created through a script (after an import, for example)
- whenever any changes have been made to models or search configuration
The search may not return any results while this command is running, so avoid running it at peak times.
.. _wagtailsearch_indexing_fields:
Indexing extra fields
=====================
@ -51,7 +80,7 @@ Fields must be explicitly added to the ``search_fields`` property of your ``Page
Example
-------------
-------
This creates an ``EventPage`` model with two fields ``description`` and ``date``. ``description`` is indexed as a ``SearchField`` and ``date`` is indexed as a ``FilterField``
@ -75,7 +104,7 @@ This creates an ``EventPage`` model with two fields ``description`` and ``date``
``index.SearchField``
-----------------------
---------------------
These are added to the search index and are used for performing full-text searches on your models. These would usually be text fields.
@ -84,12 +113,12 @@ Options
```````
- **partial_match** (boolean) - Setting this to true allows results to be matched on parts of words. For example, this is set on the title field by default so a page titled "Hello World!" will be found if the user only types "Hel" into the search box.
- **boost** (number) - This allows you to set fields as being more important than others. Setting this to a high number on a field will make pages with matches in that field to be ranked higher. By default, this is set to 100 on the title field and 1 on all other fields.
- **boost** (number) - This allows you to set fields as being more important than others. Setting this to a high number on a field will make pages with matches in that field to be ranked higher. By default, this is set to 2 on the Page title field and 1 on all other fields.
- **es_extra** (dict) - This field is to allow the developer to set or override any setting on the field in the ElasticSearch mapping. Use this if you want to make use of any ElasticSearch features that are not yet supported in Wagtail.
``index.FilterField``
-----------------------
---------------------
These are added to the search index but are not used for full-text searches. Instead, they allow you to run filters on your search results.
@ -128,6 +157,8 @@ One use for this is indexing ``get_*_display`` methods Django creates automatica
)
.. _wagtailsearch_indexing_models:
Indexing non-page models
========================

View file

@ -1,4 +1,67 @@
.. _wagtailsearch_searching:
=========
Searching
=========
.. _wagtailsearch_searching_pages:
Searching Pages
===============
Wagtail provides a ``search`` method on the QuerySet for all page models:
.. code-block:: python
# Search future EventPages
>>> from wagtail.wagtailcore.models import EventPage
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!")
All methods of ``PageQuerySet`` are supported by wagtailsearch:
.. code-block:: python
# Search all live EventPages that are under the events index
>>> EventPage.objects.live().descendant_of(events_index).search("Event")
[<EventPage: Event 1>, <EventPage: Event 2>]
Searching Images, Documents and custom models
=============================================
You can search these by using the ``search`` method on the search backend:
.. code-block:: python
>>> from wagtail.wagtailimages.models import Image
>>> from wagtail.wagtailsearch.backends import get_search_backend
# Search images
>>> s = get_search_backend()
>>> s.search("Hello", Image)
[<Image: Hello>, <Image: Hello world!>]
You can also pass a QuerySet into the ``search`` method which allows you to add filters to your search results:
.. code-block:: python
>>> from wagtail.wagtailimages.models import Image
>>> from wagtail.wagtailsearch.backends import get_search_backend
# Search images
>>> s = get_search_backend()
>>> s.search("Hello", Image.objects.filter(uploaded_by_user=user))
[<Image: Hello>]
This should work the same way for Documents and custom models as well.
.. _wagtailsearch_frontend_views:
@ -166,4 +229,47 @@ In this template, you'll have access to the same context variables provided to t
Custom Search Views
-------------------
This functionality is still under active development to provide a streamlined interface, but take a look at ``wagtail/wagtail/wagtailsearch/views/frontend.py`` if you are interested in coding custom search views.
This functionality is still under active development to provide a streamlined interface, but take a look at ``wagtail/wagtail/wagtailsearch/views/frontend.py`` if you are interested in coding custom search views.
.. _editors-picks:
Editor's picks
==============
Editor's picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content in addition to results from the search algorithm. In a template using the search results view, editor's picks can be accessed through the variable ``query.editors_picks``. To include editor's picks in your search results template, use the following properties.
``query.editors_picks.all``
This gathers all of the editor's picks objects relating to the current query, in order according to their sort order in the Wagtail admin. You can then iterate through them using a ``{% for ... %}`` loop. Each editor's pick object provides these properties:
``editors_pick.page``
The page object associated with the pick. Use ``{% pageurl editors_pick.page %}`` to generate a URL or provide other properties of the page object.
``editors_pick.description``
The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms.
Putting this all together, a block of your search results template displaying editor's picks might look like this:
.. code-block:: django
{% with query.editors_picks.all as editors_picks %}
{% if editors_picks %}
<div class="well">
<h3>Editors picks</h3>
<ul>
{% for editors_pick in editors_picks %}
<li>
<h4>
<a href="{% pageurl editors_pick.page %}">
{{ editors_pick.page.title }}
</a>
</h4>
<p>{{ editors_pick.description|safe }}</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}

View file

@ -7,6 +7,7 @@ It supports Django 1.6.2+ and 1.7rc3+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4.
.. toctree::
:maxdepth: 3
:titlesonly:
getting_started/index
core_components/index

View file

@ -47,7 +47,7 @@ Options:
update_index
------------
:code:`./manage.py update_index`
:code:`./manage.py update_index [--backend <backend name>]`
This command rebuilds the search index from scratch. It is only required when using Elasticsearch.
@ -59,6 +59,24 @@ It is recommended to run this command once a week and at the following times:
The search may not return any results while this command is running, so avoid running it at peak times.
Specifying which backend to update
``````````````````````````````````
.. versionadded:: 0.7
By default, ``update_index`` will rebuild all the search indexes listed in ``WAGTAILSEARCH_BACKENDS``.
If you have multiple backends and would only like to update one of them, you can use the ``--backend`` option.
For example, to update just the default backend:
.. code-block:: none
python manage.py update_index --backend default
.. _search_garbage_collect:
search_garbage_collect

View file

@ -180,7 +180,7 @@ The old names will continue to work, but output a ``DeprecationWarning`` - you a
New search field configuration format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``indexed_fields`` is now deprecated and has been replaced by a new search field configuration format called ``search_fields``. See :ref:`wagtailsearch_for_python_developers` for how to define a ``search_fields`` property on your models.
``indexed_fields`` is now deprecated and has been replaced by a new search field configuration format called ``search_fields``. See :ref:`wagtailsearch_indexing` for how to define a ``search_fields`` property on your models.
``Page.route`` method should now return a ``RouteResult``

View file

@ -10,8 +10,22 @@ Wagtail 0.7 release notes - IN DEVELOPMENT
What's new
==========
New interface for choosing images focal point
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
New interface for choosing image focal point
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When editing images, users can now specify a 'focal point' region that cropped versions of the image will be centred on. Previously the focal point could only be set automatically, through image feature detection.
Groups and Sites administration interfaces
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The main navigation menu has been reorganised, placing site configuration options in a 'Settings' submenu. This includes two new items, which were previously only available through the Django admin backend: 'Groups', for setting up user groups with a specific set of permissions, and 'Sites', for managing the list of sites served by this Wagtail instance.
Page locking
~~~~~~~~~~~~
Moderators and administrators now have the ability to lock a page, preventing further edits from being made to that page until it is unlocked again.
Minor features
@ -22,17 +36,50 @@ Minor features
* Page models now support a ``parent_page_types`` property in addition to ``subpage types``, to restrict the types of page they can be created under.
* ``register_snippet`` can now be invoked as a decorator.
* The project template (used when running ``wagtail start``) has been updated to Django 1.7.
* The 'boost' applied to the title field on searches has been reduced from 100 to 2.
* The ``type`` method of ``PageQuerySet`` (used to filter the queryset to a specific page type) now includes subclasses of the given page type.
* The ``update_index`` management command now updates all backends listed in ``WAGTAILSEARCH_BACKENDS``, or a specific one passed on the command line, rather than just the default backend.
* The 'fill' image resize method now supports an additional parameter defining the closeness of the crop. See :ref:`image_tag`
Bug fixes
~~~~~~~~~
* The 'wagtail start' command now works on Windows and other environments where the ``django-admin.py`` executable is not readily accessible.
* The external image URL generator no longer stores generated images in Django's cache; this was an unintentional side-effect of setting cache control headers.
* The Elasticsearch backend can now search querysets that have been filtered with an 'in' clause of a non-list type (such as a ``ValuesListQuerySet``).
* Logic around the ``has_unpublished_changes`` flag has been fixed, to prevent issues with the 'View draft' button failing to show in some cases.
* It is now easier to move pages to the beginning and end of their section
* Image rendering no longer creates erroneous duplicate Rendition records when the focal point is blank.
Upgrade considerations
======================
Addition of ``wagtailsites`` app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Sites administration interface is contained within a new app, ``wagtailsites``. To enable this on an existing Wagtail project, add the line::
'wagtail.wagtailsites',
to the ``INSTALLED_APPS`` list in your project's settings file.
Title boost on search reduced to 2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Wagtail's search interface applies a 'boost' value to give extra weighting to matches on the title field. The original boost value of 100 was found to be excessive, and in Wagtail 0.7 this has been reduced to 2. If you have used comparable boost values on other fields, to give them similar weighting to title, you may now wish to reduce these accordingly. See :ref:`wagtailsearch_indexing`.
Addition of ``locked`` field to Page model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The page locking mechanism adds a ``locked`` field to wagtailcore.Page, defaulting to False. Any application code working with Page objects should be unaffected, but any code that creates page records using direct SQL, or within existing South migrations using South's frozen ORM, will fail as this code will be unaware of the new database column. To fix a South migration that fails in this way, add the following line to the ``'wagtailcore.page'`` entry at the bottom of the migration file:
.. code-block:: python
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
Deprecated features
===================

View file

@ -33,6 +33,7 @@
<li><a href="#progress">Progress indicators</a></li>
<li><a href="#misc">Misc formatters</a></li>
<li><a href="#icons">Icons</a></li>
<li><a href="#ie9">IE9 debugging</a></li>
</ul>
</nav>
@ -502,6 +503,7 @@
<li class="icon icon-pick">pick</li>
<li class="icon icon-redirect">redirect</li>
<li class="icon icon-view">view</li>
<li class="icon icon-no-view">no-view</li>
<li class="icon icon-collapse-up">collapse-up</li>
<li class="icon icon-collapse-down">collapse-down</li>
<li class="icon icon-help">help</li>
@ -514,6 +516,15 @@
</ul>
</section>
<section id="ie9">
<h2>IE9 debugging</h2>
<p>Internet Explorer 9 has two critical limitations in its CSS support: a maximum of 31 stylesheets per page and a maximum of 4096 selectors per stylesheet. The latter is particularly problematic when CSS is concatenated.</p>
<div id="ie9-debug"></div>
</section>
</div>
{% endblock %}
@ -521,6 +532,30 @@
{% block extra_js %}
<script>
$(function(){
// Debugging for stylesheet problems
var styleSheets = document.styleSheets, totalStyleSheets = styleSheets.length;
for (var j = 0; j < totalStyleSheets; j++) {
var styleSheet = styleSheets[j], rules = styleSheet.cssRules, totalSelectorsInStylesheet = 0, style = "";
var totalRulesInStylesheet = rules ? rules.length : 0;
for (var i = 0; i < totalRulesInStylesheet; i++) {
if (rules[i].selectorText) {
try {
totalSelectorsInStylesheet += rules[i].selectorText.split(',').length;
}
catch (err) {
console.log(err);
}
}
}
if(totalSelectorsInStylesheet > 4095){
style = 'color:red';
}
$('#ie9-debug').append("<h3>" + styleSheet.href + "</h3>" + "<p>Total rules: <strong>" + totalRulesInStylesheet + "</strong>. " + "Total selectors: <strong style='" + style + "'>" + totalSelectorsInStylesheet + "</strong></p>");
}
(function runprogress(){
var to = setTimeout(function(){
runprogress();

View file

@ -47,6 +47,7 @@ INSTALLED_APPS = (
'wagtail.wagtaildocs',
'wagtail.wagtailsnippets',
'wagtail.wagtailusers',
'wagtail.wagtailsites',
'wagtail.wagtailimages',
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',

View file

@ -435,7 +435,15 @@
"permission_type": "edit"
}
},
{
"pk": 6,
"model": "wagtailcore.grouppagepermission",
"fields": {
"group": ["Event moderators"],
"page": 3,
"permission_type": "lock"
}
},
{
"pk": 1,
"model": "tests.customuser",

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('tests', '0003_auto_20140905_0634'),
]
operations = [
migrations.DeleteModel(
name='SearchTestOldConfig',
),
migrations.DeleteModel(
name='SearchTestOldConfigList',
),
]

View file

@ -65,6 +65,7 @@ INSTALLED_APPS = [
'wagtail.wagtaildocs',
'wagtail.wagtailsnippets',
'wagtail.wagtailusers',
'wagtail.wagtailsites',
'wagtail.wagtailimages',
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',

View file

@ -499,6 +499,17 @@ class BasePageChooserPanel(BaseChooserPanel):
return cls._target_content_type
def render_as_field(self, show_help_text=True):
instance_obj = self.get_chosen_item()
return mark_safe(render_to_string(self.field_template, {
'field': self.bound_field,
self.object_type_name: instance_obj,
'is_chosen': bool(instance_obj),
'show_help_text': show_help_text,
'choose_another_text_str': ugettext_lazy("Choose another page"),
'choose_one_text_str': ugettext_lazy("Choose a page"),
}))
def render_js(self):
page = self.get_chosen_item()
parent = page.get_parent() if page else None

View file

@ -8,10 +8,10 @@ try:
except ImportError:
from django.forms.util import flatatt
from django.conf import settings
from django.forms import MediaDefiningClass
from django.forms import MediaDefiningClass, Media
from django.utils.text import slugify
from django.utils.html import format_html, format_html_join
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from wagtail.wagtailcore import hooks
@ -36,21 +36,83 @@ class MenuItem(with_metaclass(MediaDefiningClass)):
"""
return True
def render_html(self):
def render_html(self, request):
return format_html(
"""<li class="menu-{0}"><a href="{1}" class="{2}"{3}>{4}</a></li>""",
self.name, self.url, self.classnames, self.attr_string, self.label)
_master_menu_item_list = None
def get_master_menu_item_list():
"""
Return the list of menu items registered with the 'register_admin_menu_item' hook.
This is the "master list" because the final admin menu may vary per request
according to the value of is_shown() and the construct_main_menu hook.
"""
global _master_menu_item_list
if _master_menu_item_list is None:
_master_menu_item_list = [fn() for fn in hooks.get_hooks('register_admin_menu_item')]
class Menu(object):
def __init__(self, register_hook_name, construct_hook_name=None):
self.register_hook_name = register_hook_name
self.construct_hook_name = construct_hook_name
# _registered_menu_items will be populated on first access to the
# registered_menu_items property. We can't populate it in __init__ because
# we can't rely on all hooks modules to have been imported at the point that
# we create the admin_menu and settings_menu instances
self._registered_menu_items = None
return _master_menu_item_list
@property
def registered_menu_items(self):
if self._registered_menu_items is None:
self._registered_menu_items = [fn() for fn in hooks.get_hooks(self.register_hook_name)]
return self._registered_menu_items
def menu_items_for_request(self, request):
return [item for item in self.registered_menu_items if item.is_shown(request)]
@property
def media(self):
media = Media()
for item in self.registered_menu_items:
media += item.media
return media
def render_html(self, request):
menu_items = self.menu_items_for_request(request)
# provide a hook for modifying the menu, if construct_hook_name has been set
if self.construct_hook_name:
for fn in hooks.get_hooks(self.construct_hook_name):
fn(request, menu_items)
rendered_menu_items = []
for item in sorted(menu_items, key=lambda i: i.order):
try:
rendered_menu_items.append(item.render_html(request))
except TypeError:
# fallback for older render_html methods that don't accept a request arg
rendered_menu_items.append(item.render_html())
return mark_safe(''.join(rendered_menu_items))
class SubmenuMenuItem(MenuItem):
"""A MenuItem which wraps an inner Menu object"""
def __init__(self, label, menu, **kwargs):
self.menu = menu
super(SubmenuMenuItem, self).__init__(label, '#', **kwargs)
@property
def media(self):
return Media(js=['wagtailadmin/js/submenu.js']) + self.menu.media
def is_shown(self, request):
# show the submenu if one or more of its children is shown
return bool(self.menu.menu_items_for_request(request))
def render_html(self, request):
return format_html(
"""<li class="menu-{0}">
<a href="#" class="submenu-trigger {1}"{2}>{3}</a>
<div class="nav-submenu">
<h2 class="{1}">{3}</h2>
<ul>{4}</ul>
</div>
</li>""",
self.name, self.classnames, self.attr_string, self.label, self.menu.render_html(request)
)
admin_menu = Menu(register_hook_name='register_admin_menu_item', construct_hook_name='construct_main_menu')
settings_menu = Menu(register_hook_name='register_settings_menu_item')

View file

@ -0,0 +1,18 @@
$(function(){
$('.nav-main .submenu-trigger').on('click', function(){
if($(this).closest('li').find('.nav-submenu').length){
$(this).closest('li').toggleClass('submenu-active');
$('.nav-wrapper').toggleClass('submenu-active')
return false
}
});
$(document).on('keydown click', function(e){
if($('.nav-wrapper.submenu-active').length){
if(e.keyCode == 27 || !e.keyCode){
$('.nav-main .submenu-active').removeClass('submenu-active');
$('.nav-wrapper').toggleClass('submenu-active')
}
}
});
});

View file

@ -121,7 +121,7 @@
color:lighten($color-grey-2,30%);
-webkit-font-smoothing: auto;
font-size:0.80em;
margin:0 0.5em;
margin:0 0.5em 0.5em 0.5em;
background:white url("#{$static-root}bg-dark-diag.svg");
&.primary{
@ -130,11 +130,17 @@
background:white
}
}
form.status-tag:hover,
a.status-tag:hover,
a.status-tag.primary:hover{
border-color:$color-teal;
color:$color-teal;
}
form.status-tag:hover{
border-color:$color-teal-dark;
background-color:$color-teal-darker;
color:white;
}
.privacy-indicator {
.label-private, .label-public{

View file

@ -297,6 +297,37 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
+ button{
margin-left:1em;
}
// A completely unstyled button
&.unbutton{
@include border-radius(0px);
width:auto;
height:auto;
line-height:auto;
padding:0;
font-size:inherit;
font-weight:normal;
vertical-align: middle;
display:inline;
background-color:transparent;
border:0;
color:inherit;
text-decoration:none;
text-transform:uppercase;
white-space: nowrap;
position:relative;
overflow:hidden;
outline:none;
box-sizing:border-box;
-webkit-font-smoothing: auto;
-moz-appearance: none;
-moz-box-sizing:border-box;
}
&:disabled, &[disabled]{
background-color:$color-grey-2;
border-color:$color-grey-2;
color:lighten($color-grey-2, 15%);
}
}
/* Special styles to counteract Firefox's completely unwarranted assumptions about button styles */

View file

@ -220,11 +220,16 @@
.icon-redirect:before{
content:"3";
}
/* Credit: Icon made by Zurb from Flaticon.com */
.icon-view:before,
.icon-no-view:before{
vertical-align:-3.5px;
font-size:1.1rem;
}
.icon-view:before{
content:"4";
vertical-align:-4.5px;
font-size:1.3rem;
}
.icon-no-view:before{
content:"^";
}
.icon-collapse-down:before{
content:"5";

View file

@ -18,7 +18,7 @@ ul.listing{
h3{
margin:0;
font-size:0.95em;
font-size:1em;
}
td, th{
@ -156,12 +156,14 @@ ul.listing{
}
.title{
color:darken($color-grey-2, 10%);
h2{
text-transform:none;
margin:0;
font-size:1.15em;
font-weight:600;
color:darken($color-grey-2, 10%);
color:inherit;
line-height:1.5em;
a{
@ -371,6 +373,9 @@ table.listing{
right:5%;
top:2em;
}
.locked-indicator{
font-size:0.8em;
}
.title{
h2{
@ -386,8 +391,10 @@ table.listing{
}
}
.privacy-indicator{
font-size:0.9em;
.privacy-indicator,
.locked-indicator{
margin-right:0;
font-size:1em;
opacity:0.7;
}

View file

@ -0,0 +1,384 @@
$selected-highlight:darken($color-grey-1, 10%);
$submenu-color:darken($color-grey-1, 5%);
.nav-wrapper{
position:relative;
background: $color-grey-1;
margin-left: -$menu-width;
width: $menu-width;
float: left;
height:100%;
min-height:800px;
}
#nav-toggle{
left:$mobile-nice-padding;
cursor:pointer;
position:absolute;
&:before{
font-size:40px;
color:white;
line-height:40px;
content:"\2261";
}
}
.nav-main{
top: 43px;
bottom: 0px;
overflow: auto;
width:100%;
ul, li{
margin:0;
padding:0;
list-style-type:none;
}
li{
@include transition(border-color 0.2s ease);
position:relative;
/* TODO: find better way to procedurally detect the appropriate menu to highlight */
.menu-snippets &.menu-snippets,
.menu-users &.menu-users,
.menu-groups &.menu-groups,
.menu-sites &.menu-sites,
.menu-redirects &.menu-redirects,
.menu-editorspicks &.menu-editors-picks,
.menu-snippets &.menu-snippets,
.menu-documents &.menu-documents,
.menu-images &.menu-images,
.menu-search &.menu-search,
.menu-explorer &.menu-explorer,
.menu-forms &.menu-forms{
background:$selected-highlight;
text-shadow:-1px -1px 0px rgba(0,0,0,0.3);
a{
border-left-color:$color-salmon;
color:white;
}
}
}
a {
@include transition(border-color 0.2s ease);
-webkit-font-smoothing: auto;
text-decoration:none;
display: block;
color: #AAA;
padding: 0.8em 1.7em;
position:relative;
font-size:0.95em;
font-weight:300;
white-space:nowrap;
border-left:3px solid transparent;
&:before{
font-size:1rem;
vertical-align:-20%;
margin-right:0.5em;
}
&:hover{
background-color:rgba(100,100,100,0.15);
color:white;
text-shadow:-1px -1px 0px rgba(0,0,0,0.3);
}
/* only really used for spinners and settings menu */
&:after{
font-size:1.5em;
margin:0;
position:absolute;
right:0.5em;
top:0.5em;
margin-top:0.15em;
}
}
.avatar{
display:none;
a:hover{
background-color:transparent;
}
}
}
.nav-submenu{
background:$submenu-color;
h2{
display:none;
}
a{
white-space:normal;
padding: 0.9em 0 0.9em 4.5em;
&:before{
margin-left:-1.5em;
}
&:hover{
background-color:rgba(100,100,100,0.2);
}
}
li{
border:0;
}
}
.explorer{
position:absolute;
margin-top:70px;
font-size:0.95em;
}
.nav-search{
position:relative;
padding:0;
margin:0 1em;
label{
@include visuallyhidden();
}
input, button{
font-size:1em;
border:0;
@include border-radius(0);
}
input{
cursor:pointer;
border:1px solid darken($color-grey-2, 10%);
background-color:darken($color-grey-2, 15%);
color:#AAA;
padding: 0.8em 2.5em 0.8em 1em;
&:hover{
background-color:darken($color-grey-2, 10%);
}
&:active, &:focus{
background-color:darken($color-grey-2, 5%);
color:white;
}
}
button{
background-color:transparent;
position:absolute;
top:0; right:0; bottom:0;
margin:auto;
padding:0;
width:3em; height:100%;
overflow:hidden;
&:before{
font-family:wagtail;
font-weight:200;
text-transform:none;
content:"f";
display:block;
height:100%;
line-height:3.3em;
padding:0 1em;
}
}
}
/* Navigation open condition */
body.nav-open{
.wrapper{
transform: translate3d($menu-width,0,0);
-webkit-transform: translate3d($menu-width,0,0);
}
.content-wrapper{
position:fixed;
}
footer{
bottom:1px;
}
}
/* Explorer open condition, widens navigation area */
body.explorer-open {
.wrapper{
transform: translate3d($menu-width*2,0,0);
-webkit-transform: translate3d($menu-width*2,0,0);
}
.nav-wrapper{
margin-left: -$menu-width*2;
width: $menu-width*2;
}
.nav-main{
display:none;
}
.explorer{
display:block;
border-top:1px solid rgba(200,200,200,0.1);
&:before{
position:absolute;
top:-3em;
content:"Close explorer";
padding:0.9em;
}
}
}
@media screen and (min-width: $breakpoint-mobile){
.wrapper,
body.nav-open .wrapper{
-webkit-transform:none;
transform:none;
padding-left:$menu-width;
}
.nav-wrapper{
/* heigt and position necessary to force it to 100% height of screen (with some JS help) */
position:absolute;
left:0;
height:100%;
margin-left: 0;
.inner{
height:100%;
position:fixed;
width:$menu-width;
}
}
#nav-toggle{
display:none;
}
.nav-main{
position:absolute;
top: 175px; /* WARNING - magic number - the height of the logo plus search box */
margin-bottom: 116px; /* WARNING - magic number - the height of the .footer */
.footer{
padding-top:1em;
background-color:$color-grey-1;
position:fixed;
width:$menu-width - 7;
bottom:0;
text-align:center;
}
.avatar{
display:block;
margin:auto;
text-align:center;
margin-bottom:1em;
a{
padding:0 0 1em 0;
}
&:hover{
@include box-shadow(0px 0px 6px 0px rgba(0,0,0,1));
}
}
a.submenu-trigger:after{
content:"n";
}
}
.nav-submenu{
position:fixed;
height:100%;
width:0;
padding:0;
top:0;
left:$menu-width;
overflow:auto;
max-height:100%;
border-right:1px solid rgba(0,0,0,0.1);
h2,ul{
float:right;
width:$menu-width;
}
h2{
display:block;
padding:0.2em 0;
font-size:1.2em;
font-weight:500;
text-transform:none;
text-align:center;
color: #AAA;
&:before{
font-size:4em;
display:block;
text-align:center;
margin:0 0 0.2em 0;
width:100%;
opacity:0.15;
}
}
}
li.submenu-active{
background:$submenu-color;
> a{
text-shadow:-1px -1px 0px rgba(0,0,0,0.3);
&:hover{
background-color:transparent;
}
}
.nav-submenu{
@include box-shadow(2px 0 2px rgba(0,0,0,0.35));
@include transition(width 0.2s ease);
width:$menu-width;
padding:0 0 1.5em 0;
a{
padding-left:3.5em;
}
}
}
.explorer {
width: 400px;
position: absolute;
top:0;
left:99%;
margin-top:175px; /* same as .nav-main minus 1 pixel for border */
}
.dl-menu {
position: absolute;
}
body.nav-open{
.content-wrapper{
position:relative;
}
}
body.explorer-open {
.wrapper{
-webkit-transform:none;
transform:none;
}
.nav-wrapper{
margin-left: 0;
width: $menu-width;
}
.explorer:before{
display:none;
}
.nav-main{
display:block;
}
}
}

View file

@ -34,6 +34,7 @@ p{
}
a{
@include transition(color 0.2s ease, background-color 0.2s ease);
outline:none;
color:$color-link;
text-decoration:none;

View file

@ -15,6 +15,7 @@
@import "components/header.scss";
@import "components/progressbar.scss";
@import "components/datetimepicker.scss";
@import "components/main-nav.scss";
@import "fonts.scss";
@ -54,219 +55,40 @@ body{
}
.wrapper{
@include transition-transform(0.2s ease);
@include clearfix();
}
.nav-wrapper{
@include box-shadow(inset -5px 0px 5px -3px rgba(0, 0, 0, 0.3));
position:relative;
background: $color-grey-1;
margin-left: -100%;
width: 180px;
float: left;
height:100%;
min-height:800px;
/* See components/main-nav.scss */
}
.logo{
display:block;
margin:2em auto;
text-align:left;
text-decoration:none;
color:white;
padding: 0.9em 1.2em;
margin:0;
font-size:0.9em;
-webkit-font-smoothing: auto;
span{
text-transform:uppercase;
}
img{
width:20px;
float:left;
border:0;
margin-right:1em;
}
}
#nav-toggle{
left:$mobile-nice-padding;
cursor:pointer;
position:absolute;
&:before{
font-size:40px;
color:white;
line-height:40px;
content:"\2261";
}
}
.nav-main{
top: 43px;
bottom: 0px;
overflow: auto;
width:100%;
ul, li{
margin:0;
padding:0;
list-style-type:none;
}
ul{
border-top:1px solid $color-grey-1-1;
}
li{
border-bottom:1px solid $color-grey-1-1;
position:relative;
&.selected{
background-color:$color-grey-1;
}
a:before{
font-size:1.2rem;
}
.menu-snippets &.menu-snippets,
.menu-users &.menu-users,
.menu-snippets &.menu-snippets,
.menu-documents &.menu-documents,
.menu-images &.menu-images,
.menu-search &.menu-search,
.menu-explorer &.menu-explorer{
background:darken($color-grey-1, 10%);
a{
color:white;
}
}
}
a {
-webkit-font-smoothing: auto;
text-decoration:none;
text-transform:uppercase;
display: block;
color: #AAA;
padding: 0.9em 1.2em;
position:relative;
font-size:0.85em;
font-weight:400;
white-space:nowrap;
text-shadow:-1px -1px 0px rgba(0,0,0,0.3);
&:before{
vertical-align:-20%;
margin-right:0.5em;
}
&:hover{
color:white;
}
/* only really used for spinners */
&:after{
font-size:1.5em;
margin:0;
position:absolute;
right:0.5em;
top:0.5em;
margin-top:0.15em;
}
}
.avatar{
display:none;
}
/* search form */
#menu-search{
position:relative;
.fields{
@include transition(background-color 0.2s ease);
border:0;
li{
border:0;
}
}
.field{
padding:0;
color: #AAA;
&:before{
position:absolute;
left:0.75em;
top:0.45em;
}
}
.submit{
@include visuallyhidden();
}
label{
font-weight:normal;
-webkit-font-smoothing: auto;
line-height:inherit;
text-transform:uppercase;
padding: 0.9em 1.2em 0.9em 3.7em;
color: #AAA;
font-size:0.95em;
}
input{
float:left;
margin-top:-1000px;
text-transform:uppercase;
font-size:1em;
padding: 0.9em 1.2em 0.9em 3.5em;
border:0;
border-radius:0;
background-color:transparent;
line-height:inherit;
}
&:hover{
.field, input{
color:white;
}
}
&.focussed{
label{
display:none;
}
.fields{
background-color:$color-grey-4;
}
.field{
color: $color-grey-1;
}
input{
margin-top:0px;
color:$color-grey-1;
}
}
}
}
.explorer{
position:absolute;
margin-top:70px;
font-size:0.9em;
.logo{
display:block;
margin:2em auto;
text-align:left;
text-decoration:none;
color: #AAA;
padding: 0.9em 1.2em;
margin:0;
-webkit-font-smoothing: auto;
img{
width:20px;
float:left;
border:0;
margin-right:1em;
}
}
.content-wrapper{
width:100%;
height:100%; /* this has no effect on desktop, but on mobile it helps aesthetics of menu popout action */
float: right;
float: left;
position: relative;
background-color:$color-grey-4;
border-bottom:1px solid $color-grey-3;
/* transform: translate3d(0,0,0);
-webkit-transform: translate3d(0,0,0);*/
}
.content{
@include row();
@ -276,52 +98,8 @@ body{
position:relative; /* yuk. necessary for positions for jquery ui widgets */
}
body.nav-open .nav-wrapper{
margin-left:0;
}
body.nav-open .content-wrapper{
transform: translate3d(180px,0,0);
-webkit-transform: translate3d(180px,0,0);
position:fixed;
}
body.nav-open footer{
bottom:1px;
}
body.nav-closed footer{
bottom:0;
}
body.explorer-open {
.nav-wrapper{
width:80%;
}
.nav-main{
display:none;
}
.content-wrapper{
transform: translate3d(80%,0,0);
-webkit-transform: translate3d(80%,0,0);
position:fixed;
}
.explorer{
display:block;
border-top:1px solid rgba(200,200,200,0.1);
&:before{
position:absolute;
top:-3em;
content:"Close explorer";
padding:0.9em;
}
}
}
footer{
@include transition(all 0.2s ease);
@include row();
@include border-radius(3px 3px 0 0);
@include box-shadow(0px 0px 2px rgba(255,255,255,0.5));
@ -420,21 +198,6 @@ footer{
}
}
/* &:after, &:before{
content: "";
border-top: 1.2em solid transparent;
border-bottom: 1.2em solid transparent;
}
&:after {
border-left: 1em solid $color-teal-darker;
position: absolute; right: -1.2em; top: 0;
z-index: 1;
border:1em solid red;
}
&:before {
border-left: 1em solid white;
position: absolute; left: 0; top: 0;
} */
&:hover {
background: $color-teal-dark;
a{
@ -501,6 +264,9 @@ footer{
.content-wrapper{
z-index:3;
}
.nav-submenu{
z-index:6;
}
footer, .logo{
z-index:100;
}
@ -556,135 +322,40 @@ footer, .logo{
padding-right:$desktop-nice-padding;
}
.wrapper{
margin-left:$menu-width;
}
.browsermessage{
margin:0 0 0 -150px;
}
.content-wrapper{
-webkit-transform:none;
transform:none;
border-bottom-right-radius: 5px;
}
}
.nav-wrapper{
/* heigt and position necessary to force it to 100% height of screen (with some JS help) */
position:absolute;
left:0;
height:100%;
width:$menu-width;
margin-left: 0;
.logo{
margin:1em auto;
text-align:center;
.inner{
height:100%;
position:fixed;
width:$menu-width;
}
}
.logo{
margin:1em auto;
text-align:center;
span{
display:none;
}
img{
width:60px;
float:none;
margin:auto;
display:block;
}
}
.content{
border-top:0;
background-color:none;
padding-top:0;
}
#nav-toggle{
display:none;
}
.nav-main{
position:absolute;
top: 125px;
margin-bottom: 116px; /* WARNING: magic number - the height of the .footer */
.footer{
padding-top:1em;
background-color:$color-grey-1;
position:fixed;
width:$menu-width - 7;
bottom:0;
text-align:center;
}
.avatar{
display:block;
margin:auto;
text-align:center;
margin-bottom:1em;
a{
padding:0 0 1em 0;
}
&:hover{
@include box-shadow(0px 0px 6px 0px rgba(0,0,0,1));
}
}
}
.explorer {
width: 400px;
position: absolute;
top:0;
left:99%;
margin-top:124px; /* same as .nav-main minus 1 pixel for border */
}
.dl-menu {
position: absolute;
}
/* UN-set the transformations used on mobile */
body.nav-open{
.content-wrapper{
position:relative;
transform: none;
-webkit-transform: none;
}
.nav-wrapper{
margin-left: -$menu-width;
}
.nav-wrapper,
.nav-main{
width:$menu-width;
}
}
body.explorer-open {
.nav-wrapper{
width:$menu-width;
}
.explorer:before{
span{
display:none;
}
.nav-main{
img{
width:60px;
float:none;
margin:auto;
display:block;
}
.content-wrapper{
transform: none;
-webkit-transform: none;
position:relative;
}
}
}
footer{
width:80%;
margin-left:50px;
}
.content{
border-top:0;
background-color:none;
padding-top:0;
}
/* END NOT SURE ABOUT THIS */
.breadcrumb{
padding-top:0;
@ -699,6 +370,12 @@ footer, .logo{
}
}
footer{
width:80%;
margin-left:50px;
}
/* Z-indexes */
.nav-main{
li{
@ -711,8 +388,14 @@ footer, .logo{
.explorer {
z-index:$explorer-z-index;
}
.nav-submenu {
z-index:$explorer-z-index;
}
.nav-wrapper{
z-index:auto; /* allows overspill of messages banner onto left menu, but also explorer to spill over main content */
z-index:auto; /* allows overspill of messages banner onto left menu, but also explorer to spill over main content */
}
.nav-wrapper.submenu-active{
z-index:5;
}
}
@ -720,9 +403,6 @@ footer, .logo{
.wrapper{
max-width:$breakpoint-desktop-larger;
}
.nav-wrapper .inner{
background:$color-grey-1;
}
footer{
width:90em;
@ -742,4 +422,4 @@ footer,
}
.nav-main a, a{
@include transition(color 0.2s ease, background-color 0.2s ease);
}
}

View file

@ -1,7 +1,7 @@
{
"metadata": {
"name": "Wagtail",
"lastOpened": 1405598127608,
"name": "Wagtail 1",
"lastOpened": 1410881728324,
"created": 1405597423787
},
"iconSets": [
@ -430,11 +430,21 @@
},
{
"id": 104,
"order": 0
"order": 66,
"prevSize": 16,
"code": 52,
"name": "eye",
"tempChar": "",
"ligatures": ""
},
{
"id": 105,
"order": 0
"order": 67,
"prevSize": 16,
"code": 94,
"name": "eye-slash",
"tempChar": "",
"ligatures": ""
},
{
"id": 106,
@ -666,7 +676,7 @@
"prevSize": 16,
"code": 64,
"name": "globe",
"tempChar": "",
"tempChar": "",
"ligatures": ""
},
{
@ -1884,7 +1894,7 @@
],
"defaultCode": 61447,
"grid": 16,
"matchesSearch": true
"matchesSearch": false
},
{
"id": 8,
@ -3090,26 +3100,26 @@
{
"id": 104,
"paths": [
"M0 548.571q0-19.429 11.429-39.429 80-130.857 215.143-210.286t285.429-79.429 285.429 79.429 215.143 210.286q11.429 20 11.429 39.429t-11.429 39.429q-80 131.429-215.143 210.571t-285.429 79.143-285.429-79.429-215.143-210.286q-11.429-20-11.429-39.429zM73.143 548.571q76 117.143 190.571 186.571t248.286 69.429 248.286-69.429 190.571-186.571q-86.857-134.857-217.714-201.714 34.857 59.429 34.857 128.571 0 105.714-75.143 180.857t-180.857 75.143-180.857-75.143-75.143-180.857q0-69.143 34.857-128.571-130.857 66.857-217.714 201.714zM338.286 475.429q0 11.429 8 19.429t19.429 8 19.429-8 8-19.429q0-49.143 34.857-84t84-34.857q11.429 0 19.429-8t8-19.429-8-19.429-19.429-8q-71.429 0-122.571 51.143t-51.143 122.571z"
"M62 512.142q0-17.077 10.045-34.655 70.313-115.011 189.091-184.822t250.866-69.811 250.866 69.811 189.091 184.822q10.045 17.578 10.045 34.655t-10.045 34.655q-70.313 115.514-189.091 185.072t-250.866 69.56-250.866-69.811-189.091-184.822q-10.045-17.578-10.045-34.655zM126.286 512.142q66.797 102.958 167.494 163.978t218.22 61.022 218.22-61.022 167.494-163.978q-76.339-118.526-191.35-177.288 30.636 52.233 30.636 113.002 0 92.913-66.044 158.956t-158.956 66.044-158.956-66.044-66.044-158.956q0-60.771 30.636-113.002-115.011 58.761-191.35 177.288zM359.322 447.858q0 10.045 7.031 17.077t17.077 7.031 17.077-7.031 7.031-17.077q0-43.192 30.636-73.828t73.828-30.636q10.045 0 17.077-7.031t7.031-17.077-7.031-17.077-17.077-7.031q-62.78 0-107.728 44.95t-44.95 107.728z"
],
"tags": [
"eye"
],
"defaultCode": 61550,
"grid": 14,
"matchesSearch": false
"grid": 16,
"matchesSearch": true
},
{
"id": 105,
"paths": [
"M0 548.571q0-21.714 11.429-39.429 87.429-134.286 217.143-212t283.429-77.714q50.857 0 102.857 9.714l30.857-55.429q5.714-9.143 16-9.143 2.857 0 10.286 3.429t17.714 8.857 18.857 10.571 18 10.571 11.143 6.571q9.143 5.714 9.143 15.429 0 4-0.571 5.143-60 107.429-180 323.429t-180.571 324l-28 50.857q-5.714 9.143-16 9.143-6.857 0-76.571-40-9.143-5.714-9.143-16 0-6.857 25.143-49.714-81.714-37.143-150.571-98.857t-119.143-140q-11.429-17.714-11.429-39.429zM73.143 548.571q95.429 147.429 244 214.286l44.571-80.571q-49.714-36-77.714-90.857t-28-116q0-69.143 34.857-128.571-130.857 66.857-217.714 201.714zM338.286 475.429q0 11.429 8 19.429t19.429 8 19.429-8 8-19.429q0-49.143 34.857-84t84-34.857q11.429 0 19.429-8t8-19.429-8-19.429-19.429-8q-71.429 0-122.571 51.143t-51.143 122.571zM512 877.714l42.286-75.429q121.143-10.286 224.286-78.286t172.286-175.429q-65.714-102.286-161.143-168l36-64q54.286 36.571 104.286 87.429t82.571 105.143q11.429 19.429 11.429 39.429t-11.429 39.429q-22.286 36.571-62.286 82.857-85.714 98.286-198.571 152.571t-239.714 54.286zM603.429 714.286l160-286.857q4.571 25.714 4.571 48 0 79.429-45.143 144.857t-119.429 94z"
"M62 512.142q0-19.085 10.045-34.655 76.842-118.025 190.849-186.328t249.108-68.303q44.698 0 90.401 8.538l27.12-48.717q5.022-8.036 14.063-8.036 2.511 0 9.040 3.014t15.569 7.784 16.573 9.291 15.82 9.291 9.794 5.775q8.036 5.022 8.036 13.561 0 3.516-0.502 4.521-52.734 94.42-158.203 284.264t-158.705 284.766l-24.609 44.698q-5.022 8.036-14.063 8.036-6.026 0-67.298-35.156-8.036-5.022-8.036-14.063 0-6.026 22.099-43.694-71.819-32.646-132.337-86.886t-104.716-123.047q-10.045-15.569-10.045-34.655zM126.286 512.142q83.873 129.577 214.453 188.337l39.173-70.814q-43.694-31.641-68.303-79.854t-24.609-101.953q0-60.771 30.636-113.002-115.011 58.761-191.35 177.288zM359.322 447.858q0 10.045 7.031 17.077t17.077 7.031 17.077-7.031 7.031-17.077q0-43.192 30.636-73.828t73.828-30.636q10.045 0 17.077-7.031t7.031-17.077-7.031-17.077-17.077-7.031q-62.78 0-107.728 44.95t-44.95 107.728zM512 801.428l37.165-66.295q106.474-9.040 197.126-68.806t151.423-154.186q-57.757-89.9-141.63-147.656l31.641-56.25q47.712 32.142 91.658 76.842t72.572 92.411q10.045 17.077 10.045 34.655t-10.045 34.655q-19.587 32.142-54.743 72.823-75.335 86.384-174.525 134.095t-210.686 47.712zM592.358 657.79l140.625-252.12q4.017 22.6 4.017 42.188 0 69.811-39.677 127.315t-104.967 82.617z"
],
"tags": [
"eye-slash"
],
"defaultCode": 61552,
"grid": 14,
"matchesSearch": false
"grid": 16,
"matchesSearch": true
},
{
"id": 106,
@ -3895,7 +3905,7 @@
],
"defaultCode": 61632,
"grid": 14,
"matchesSearch": true
"matchesSearch": false
},
{
"id": 169,
@ -4471,7 +4481,7 @@
],
"defaultCode": 61680,
"grid": 14,
"matchesSearch": true
"matchesSearch": false
},
{
"id": 214,
@ -7330,7 +7340,7 @@
"code": 56,
"name": "clock",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"order": 3,
@ -7339,7 +7349,7 @@
"code": 109,
"name": "lock",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"order": 8,
@ -7348,7 +7358,7 @@
"code": 112,
"name": "lock-open",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"order": 1,
@ -7357,7 +7367,7 @@
"code": 36,
"name": "form",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 0,
@ -7366,7 +7376,7 @@
"code": 97,
"name": "uni61",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 1,
@ -7375,7 +7385,7 @@
"code": 98,
"name": "uni62",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 2,
@ -7384,7 +7394,7 @@
"code": 99,
"name": "uni63",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 3,
@ -7393,7 +7403,7 @@
"code": 100,
"name": "uni64",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 4,
@ -7402,7 +7412,7 @@
"code": 101,
"name": "uni65",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 5,
@ -7411,7 +7421,7 @@
"code": 102,
"name": "uni66",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 6,
@ -7420,7 +7430,7 @@
"code": 103,
"name": "uni67",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 7,
@ -7429,7 +7439,7 @@
"code": 105,
"name": "uni69",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 8,
@ -7438,7 +7448,7 @@
"code": 106,
"name": "uni6A",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 9,
@ -7447,7 +7457,7 @@
"code": 107,
"name": "uni6B",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 10,
@ -7456,7 +7466,7 @@
"code": 108,
"name": "uni6C",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 12,
@ -7465,7 +7475,7 @@
"code": 110,
"name": "uni6E",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 13,
@ -7474,7 +7484,7 @@
"code": 104,
"name": "uni68",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 14,
@ -7483,7 +7493,7 @@
"code": 111,
"name": "uni6F",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 16,
@ -7492,7 +7502,7 @@
"code": 113,
"name": "uni71",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 17,
@ -7501,7 +7511,7 @@
"code": 114,
"name": "uni72",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 18,
@ -7510,7 +7520,7 @@
"code": 115,
"name": "uni73",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 19,
@ -7519,7 +7529,7 @@
"code": 116,
"name": "uni74",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 20,
@ -7528,7 +7538,7 @@
"code": 117,
"name": "uni75",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 21,
@ -7537,7 +7547,7 @@
"code": 118,
"name": "uni76",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 22,
@ -7546,7 +7556,7 @@
"code": 119,
"name": "uni77",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 23,
@ -7555,7 +7565,7 @@
"code": 120,
"name": "uni78",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 24,
@ -7564,7 +7574,7 @@
"code": 122,
"name": "uni7A",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 25,
@ -7573,7 +7583,7 @@
"code": 65,
"name": "uni41",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 26,
@ -7582,7 +7592,7 @@
"code": 66,
"name": "uni42",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 27,
@ -7591,7 +7601,7 @@
"code": 68,
"name": "uni44",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 28,
@ -7600,7 +7610,7 @@
"code": 67,
"name": "uni43",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 29,
@ -7609,7 +7619,7 @@
"code": 69,
"name": "uni45",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 30,
@ -7618,7 +7628,7 @@
"code": 70,
"name": "uni46",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 31,
@ -7627,7 +7637,7 @@
"code": 71,
"name": "uni47",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 32,
@ -7636,7 +7646,7 @@
"code": 72,
"name": "uni48",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 33,
@ -7645,7 +7655,7 @@
"code": 73,
"name": "uni49",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 34,
@ -7654,7 +7664,7 @@
"code": 74,
"name": "uni4A",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 35,
@ -7663,7 +7673,7 @@
"code": 75,
"name": "uni4B",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 36,
@ -7672,7 +7682,7 @@
"code": 76,
"name": "uni4C",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 37,
@ -7681,7 +7691,7 @@
"code": 77,
"name": "uni4D",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 38,
@ -7690,7 +7700,7 @@
"code": 78,
"name": "uni4E",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 39,
@ -7699,7 +7709,7 @@
"code": 79,
"name": "uni4F",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 40,
@ -7708,7 +7718,7 @@
"code": 80,
"name": "uni50",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 41,
@ -7717,7 +7727,7 @@
"code": 81,
"name": "uni51",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 42,
@ -7735,7 +7745,7 @@
"code": 82,
"name": "uni52",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 44,
@ -7744,7 +7754,7 @@
"code": 84,
"name": "uni54",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 45,
@ -7753,7 +7763,7 @@
"code": 87,
"name": "uni57",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 46,
@ -7762,7 +7772,7 @@
"code": 88,
"name": "uni58",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 47,
@ -7771,7 +7781,7 @@
"code": 89,
"name": "uni59",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 48,
@ -7780,7 +7790,7 @@
"code": 90,
"name": "uni5A",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 49,
@ -7789,7 +7799,7 @@
"code": 86,
"name": "uni56",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 50,
@ -7798,7 +7808,7 @@
"code": 49,
"name": "uni31",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 51,
@ -7807,7 +7817,7 @@
"code": 85,
"name": "uni55",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 52,
@ -7816,7 +7826,7 @@
"code": 51,
"name": "uni33",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 53,
@ -7825,7 +7835,7 @@
"code": 50,
"name": "uni32",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 54,
@ -7834,7 +7844,7 @@
"code": 53,
"name": "uni35",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 55,
@ -7843,7 +7853,7 @@
"code": 54,
"name": "uni36",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 56,
@ -7852,7 +7862,7 @@
"code": 48,
"name": "uni30",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 57,
@ -7861,7 +7871,7 @@
"code": 63,
"name": "uni3F",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 58,
@ -7870,7 +7880,7 @@
"code": 33,
"name": "uni21",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 59,
@ -7879,7 +7889,7 @@
"code": 57,
"name": "uni39",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 60,
@ -7888,25 +7898,25 @@
"code": 83,
"name": "uni53",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 61,
"order": 65,
"order": 0,
"prevSize": 16,
"code": 52,
"name": "uni34",
"ligatures": "",
"tempChar": ""
"tempChar": ""
},
{
"id": 62,
"order": 66,
"order": 65,
"prevSize": 16,
"code": 55,
"name": "uni37",
"ligatures": "",
"tempChar": ""
"tempChar": ""
}
],
"id": 0,

View file

@ -13,7 +13,7 @@
<glyph unicode="&#x31;" d="M106.861 74.861c-39.551 39.551-61.798 93.109-61.798 149.139 0 116.18 94.757 210.938 210.938 210.938l26.367-26.367-26.367-26.367c-87.341 0-158.203-70.862-158.203-158.203 0-42.022 16.48-82.397 46.143-112.061l-32.135-4.12zM256 13.063l-26.367 26.367 26.367 26.367c87.341 0 158.203 70.862 158.203 158.203 0 42.023-16.48 82.397-46.143 112.061l32.135 4.12 4.944 32.959c39.551-39.551 61.798-93.109 61.798-149.139 0-116.18-94.757-210.938-210.938-210.938z" />
<glyph unicode="&#x32;" d="M203.266 276.735h-158.203l122.772-89.814-44.495-147.492 126.068 88.166 138.428-88.166-56.854 147.491 135.956 89.813h-158.203l-59.327 151.612z" />
<glyph unicode="&#x33;" d="M327.25 383.375v-88.125h45.938v-91.875h-180v51.563l-142.5-95.625 142.5-94.688v50.625h268.125v268.125z" />
<glyph unicode="&#x34;" d="M256 286.053c9.366 0 17.562-1.171 25.758-4.683s15.221-7.025 21.075-12.88c5.854-5.854 10.538-12.88 12.88-19.903 3.513-8.196 5.854-16.391 5.854-24.588 0-8.195-2.341-16.391-5.854-24.587-2.341-7.025-7.025-14.050-12.88-19.904s-12.88-9.366-21.075-12.88c-8.195-3.513-16.391-4.683-25.758-4.683s-17.562 1.171-25.758 4.683c-8.196 3.513-15.221 7.025-21.075 12.88-5.854 5.854-10.538 12.88-12.88 19.904-3.513 8.195-5.854 16.391-5.854 24.587s2.341 16.391 5.854 24.588c2.341 7.025 7.025 14.050 12.88 19.903s12.88 9.367 21.075 12.88c8.196 3.513 16.391 4.683 25.758 4.683zM256 126.822c14.050 0 26.928 2.341 39.808 7.025s23.416 11.709 32.783 21.075c9.366 8.195 16.391 18.733 22.245 30.441 4.683 12.88 7.025 24.587 7.025 38.637s-2.341 25.758-7.025 38.637c-5.854 11.709-12.88 22.245-22.245 30.441-9.366 9.366-19.904 16.391-32.783 21.074s-25.758 7.025-39.808 7.025-26.928-2.341-39.808-7.025c-12.88-4.683-23.416-11.708-32.783-21.074-9.367-8.196-16.391-18.733-22.245-30.441-4.683-12.88-7.025-24.588-7.025-38.637s2.341-25.758 7.025-38.637c5.854-11.709 12.88-22.245 22.245-30.441 9.367-9.366 19.904-16.391 32.783-21.075s25.758-7.025 39.808-7.025zM256 353.961c21.075 0 40.979-2.341 59.712-7.025s36.296-11.709 52.687-19.904c16.391-7.025 30.441-16.391 43.32-25.758 12.88-10.538 24.587-19.904 33.954-29.27 8.195-9.367 15.221-18.733 19.904-26.929 5.854-9.367 8.195-16.391 8.195-21.075s-2.341-11.708-8.195-21.075c-4.683-8.195-11.709-17.562-19.904-26.929-9.366-9.366-21.075-18.733-33.954-29.27-12.88-9.366-26.928-18.733-43.32-25.758-16.391-8.195-33.954-15.221-52.687-19.904s-38.637-7.025-59.712-7.025c-21.075 0-40.979 2.341-59.712 7.025-19.904 4.683-36.296 11.709-52.686 19.904-16.391 7.025-30.441 16.391-43.321 25.758-12.88 10.538-24.588 19.904-33.954 29.27-8.195 9.366-15.221 18.733-19.904 26.929-5.854 9.366-8.196 16.391-8.196 21.075s2.341 11.709 8.195 21.075c4.683 8.196 11.709 17.562 19.904 26.929 9.367 9.367 21.075 18.733 33.954 29.27 12.88 9.367 26.929 18.733 43.321 25.758 16.391 8.196 32.783 15.221 52.686 19.904 18.733 4.683 38.637 7.025 59.712 7.025z" />
<glyph unicode="&#x34;" d="M31 223.929q0 8.538 5.023 17.327 35.157 57.505 94.546 92.411t125.433 34.906 125.433-34.906 94.546-92.411q5.022-8.789 5.022-17.327t-5.022-17.327q-35.156-57.757-94.546-92.536t-125.433-34.78-125.433 34.906-94.546 92.411q-5.023 8.789-5.023 17.327zM63.143 223.929q33.398-51.479 83.747-81.989t109.11-30.511 109.11 30.511 83.747 81.989q-38.17 59.263-95.675 88.644 15.318-26.116 15.318-56.501 0-46.457-33.022-79.478t-79.478-33.022-79.478 33.022-33.022 79.478q0 30.385 15.318 56.501-57.505-29.38-95.675-88.644zM179.661 256.071q0-5.023 3.516-8.539t8.538-3.516 8.538 3.516 3.516 8.539q0 21.596 15.318 36.914t36.914 15.318q5.022 0 8.538 3.516t3.515 8.538-3.515 8.538-8.538 3.516q-31.39 0-53.864-22.475t-22.475-53.864z" />
<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" />
@ -47,6 +47,7 @@
<glyph unicode="&#x58;" d="M351.801 122.926c0.879-3.515 0.879-6.152-1.758-8.789l-87.89-96.68c-0.879-1.758-3.515-2.637-5.274-2.637-2.637 0-4.394 0.879-6.153 2.637l-88.769 96.68c-2.637 2.637-2.637 5.274-1.758 8.789 1.758 2.637 4.394 4.394 7.031 4.394h56.25v313.769c0 2.637 0.879 4.394 2.637 5.274 1.758 1.758 3.516 2.637 6.153 2.637h47.461c2.637 0 4.394-0.879 6.152-2.637 1.758-0.879 2.637-2.637 2.637-5.274v-313.769h56.25c3.515 0 5.274-1.758 7.031-4.394z" />
<glyph unicode="&#x59;" d="M351.801 325.074c-1.758-2.637-4.394-4.394-7.031-4.394h-56.25v-313.769c0-2.637-0.879-4.394-2.637-5.274-1.758-1.758-3.515-2.637-6.152-2.637h-47.461c-2.637 0-4.394 0.879-6.153 2.637-1.758 0.879-2.637 2.637-2.637 5.274v313.769h-56.25c-3.516 0-5.274 1.758-7.031 4.394-0.879 3.516-0.879 6.152 1.758 8.789l87.891 96.68c0.879 1.758 3.516 2.636 5.274 2.636 2.636 0 4.395-0.879 6.153-2.636l88.769-96.68c2.637-2.637 2.637-5.274 1.758-8.789z" />
<glyph unicode="&#x5a;" d="M201 270v-165c0-3-1-5-2-6-2-2-4-3-7-3h-18c-3 0-5 1-7 3-2 1-2 3-2 6v165c0 2 0 5 2 6 2 2 4 3 7 3h18c3 0 5-1 7-3 1-1 2-4 2-6zM274 270v-165c0-3-1-5-2-6-2-2-4-3-7-3h-18c-3 0-5 1-7 3-1 1-2 3-2 6v165c0 2 1 5 2 6 2 2 4 3 7 3h18c3 0 5-1 7-3 1-1 2-4 2-6zM347 270v-165c0-3 0-5-2-6-2-2-4-3-7-3h-18c-3 0-5 1-7 3-1 1-2 3-2 6v165c0 2 1 5 2 6 2 2 4 3 7 3h18c3 0 5-1 7-3 2-1 2-4 2-6zM384 63v271h-256v-271c0-4 1-8 2-12 1-3 3-6 4-7 2-2 3-3 3-3h238c0 0 1 1 3 3 1 1 3 4 4 7 1 4 2 8 2 12zM192 370h128l-14 34c-1 1-3 2-5 3h-90c-2-1-4-2-5-3zM457 361v-18c0-3-1-5-2-7-2-1-4-2-7-2h-27v-271c0-16-5-30-14-41-9-12-20-17-32-17h-238c-12 0-23 5-32 16s-14 25-14 41v272h-27c-3 0-5 1-7 2-1 2-2 4-2 7v18c0 3 1 5 2 7 2 1 4 2 7 2h88l20 48c3 7 8 13 16 18 7 5 15 7 22 7h92c7 0 15-2 22-7 8-5 13-11 16-18l20-48h88c3 0 5-1 7-2 1-2 2-4 2-7z" />
<glyph unicode="&#x5e;" d="M31 223.929q0 9.542 5.023 17.327 38.421 59.012 95.424 93.164t124.554 34.151q22.349 0 45.2-4.269l13.56 24.358q2.511 4.018 7.031 4.018 1.255 0 4.52-1.507t7.784-3.892 8.286-4.646 7.91-4.646 4.897-2.887q4.018-2.511 4.018-6.781 0-1.758-0.251-2.26-26.367-47.21-79.101-142.132t-79.353-142.383l-12.304-22.349q-2.511-4.018-7.031-4.018-3.013 0-33.649 17.578-4.018 2.511-4.018 7.031 0 3.013 11.049 21.847-35.91 16.323-66.168 43.443t-52.358 61.524q-5.023 7.784-5.023 17.327zM63.143 223.929q41.936-64.788 107.227-94.168l19.587 35.407q-21.847 15.82-34.151 39.927t-12.304 50.976q0 30.386 15.318 56.501-57.505-29.381-95.675-88.644zM179.661 256.071q0-5.023 3.516-8.539t8.538-3.516 8.538 3.516 3.516 8.539q0 21.596 15.318 36.914t36.914 15.318q5.022 0 8.538 3.516t3.515 8.538-3.515 8.538-8.538 3.516q-31.39 0-53.864-22.475t-22.475-53.864zM256 79.286l18.582 33.147q53.237 4.52 98.563 34.403t75.712 77.093q-28.878 44.95-70.815 73.828l15.82 28.125q23.856-16.071 45.829-38.421t36.286-46.205q5.022-8.538 5.022-17.327t-5.022-17.327q-9.793-16.071-27.372-36.411-37.668-43.192-87.262-67.048t-105.343-23.856zM296.179 151.105l70.313 126.060q2.009-11.3 2.009-21.094 0-34.906-19.839-63.658t-52.483-41.308z" />
<glyph unicode="&#x61;" d="M239 224c0 19-7 35-20 48-13 14-29 20-48 20s-35-6-49-20c-13-13-20-29-20-48s7-35 20-48c14-14 30-20 49-20s35 6 48 20c13 13 20 29 20 48zM444 87c0 10-4 18-10 24-7 7-15 11-24 11-10 0-18-4-24-11-7-6-11-14-11-24 0-9 4-17 10-24 7-6 15-10 25-10 9 0 17 4 24 10 6 7 10 15 10 24zM444 361c0 9-4 17-10 24-7 6-15 10-24 10-10 0-18-4-24-10-7-7-11-15-11-24 0-10 4-18 10-25 7-6 15-10 25-10 9 0 17 4 24 10 6 7 10 15 10 25zM341 248v-49c0-2 0-4-2-5-1-2-2-3-4-3l-41-6c-2-7-5-13-9-21 6-8 14-18 24-30 2-2 2-4 2-6s0-4-2-5c-4-5-11-13-22-24-10-10-17-16-21-16-2 0-3 1-5 2l-31 24c-6-3-13-6-20-8-2-19-4-33-7-41-1-5-3-7-8-7h-49c-2 0-4 1-5 2-2 2-3 3-3 5l-6 41c-6 2-13 4-20 8l-32-24c-1-1-3-2-5-2s-4 1-6 3c-25 23-38 37-38 42 0 2 1 4 2 5 2 3 5 8 11 14 5 7 9 13 12 17-4 8-7 15-9 22l-41 6c-1 0-3 1-4 3-1 1-2 3-2 5v49c0 2 1 4 2 5 1 2 3 3 4 3l41 6c2 7 5 13 9 21-6 8-14 18-24 30-1 2-2 4-2 6s1 4 2 5c4 5 11 13 22 24 11 10 18 16 21 16 2 0 4-1 6-2l30-24c6 3 13 6 21 8 2 19 4 33 6 41 1 5 4 7 8 7h49c2 0 4-1 6-2 1-2 2-3 2-5l7-41c6-2 12-4 20-8l31 24c1 1 3 2 5 2s4-1 6-3c26-23 38-37 38-42 0-2 0-4-1-5-3-3-6-8-12-15-5-6-9-12-12-16 4-8 7-16 9-22l41-6c2 0 3-1 4-3 2-1 2-3 2-5zM512 106v-37c0-3-13-6-40-8-2-5-5-10-8-14 9-20 14-33 14-37 0-1 0-1-1-2-22-13-33-19-33-19-2 0-6 4-13 13-6 8-11 14-13 18-4-1-7-1-8-1-2 0-5 0-8 1-3-4-8-10-14-18-7-9-11-13-13-13 0 0-11 6-33 19 0 1-1 1-1 2 0 4 5 17 14 37-3 4-6 9-8 14-27 2-40 5-40 8v37c0 3 13 6 40 8 2 6 5 10 8 14-9 20-14 33-14 37 0 1 1 1 1 2 1 0 4 2 10 5 5 3 10 6 15 9s8 5 8 5c2 0 6-5 13-13 6-8 11-14 14-18 3 1 6 1 8 1 1 0 4 0 8-1 9 13 17 23 24 30l2 1c0 0 11-7 33-19 1-1 1-1 1-2 0-4-5-17-14-37 3-4 6-8 8-14 27-2 40-5 40-8zM512 379v-37c0-3-13-6-40-8-2-5-5-10-8-14 9-20 14-33 14-37 0-1 0-1-1-2-22-13-33-19-33-19-2 0-6 4-13 13-6 8-11 14-13 18-4-1-7-1-8-1-2 0-5 0-8 1-3-4-8-10-14-18-7-9-11-13-13-13 0 0-11 6-33 19 0 1-1 1-1 2 0 4 5 17 14 37-3 4-6 9-8 14-27 2-40 5-40 8v37c0 3 13 6 40 8 2 6 5 10 8 14-9 20-14 33-14 37 0 1 1 1 1 2 1 0 4 2 10 5 5 4 10 7 15 9 5 3 8 5 8 5 2 0 6-4 13-13 6-8 11-14 14-18 3 1 6 1 8 1 1 0 4 0 8-1 9 13 17 23 24 30l2 1c0 0 11-7 33-19 1-1 1-1 1-2 0-4-5-17-14-37 3-4 6-8 8-14 27-2 40-5 40-8z" />
<glyph unicode="&#x62;" d="M451.2 263.466v-244.267c0-8.534-3.2-14.933-8.534-20.267-5.334-6.4-12.8-8.534-21.334-8.534h-330.666c-8.534 0-16 2.134-21.334 8.534-5.334 5.334-8.534 11.733-8.534 20.267v409.6c0 8.534 3.2 14.933 8.534 20.267 5.334 6.4 12.8 8.534 21.334 8.534h165.333v-165.333c0-8.534 3.2-14.934 8.533-20.267 5.334-6.4 12.8-8.534 20.267-8.534zM450.134 301.866h-154.667v155.734c16-3.2 29.866-9.6 39.466-20.267l94.933-94.934c10.666-10.666 17.067-23.466 20.267-40.534z" />
<glyph unicode="&#x63;" d="M100.266 28.8h311.466v234.666h-126.933c-7.466 0-14.933 2.134-20.267 8.534-5.334 5.334-8.534 11.734-8.534 20.267v126.933h-155.733zM295.466 301.866h114.134c-2.134 6.4-4.267 10.667-6.4 12.8l-96 94.933c-2.134 3.2-6.4 5.333-11.733 7.466zM451.2 292.267v-273.067c0-8.534-3.2-14.933-8.534-20.267-5.334-6.4-12.8-8.534-21.334-8.534h-330.666c-8.534 0-16 2.134-21.334 8.534-5.334 5.334-8.534 11.733-8.534 20.267v409.6c0 8.534 3.2 14.933 8.534 20.267 5.334 6.4 12.8 8.534 21.334 8.534h194.133c8.534 0 17.067-1.067 27.733-5.334 9.6-4.267 17.067-9.6 22.4-14.933l94.933-94.934c6.4-5.333 10.666-13.866 14.933-23.466s6.4-18.133 6.4-26.666z" />

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -62,4 +62,4 @@ $color-text-input: $color-grey-1;
/* misc sizing */
$thumbnail-width: 130px;
$menu-width: 150px;
$menu-width: 180px;

View file

@ -5,6 +5,15 @@
<div class="nav-wrapper">
<div class="inner">
<a href="{% url 'wagtailadmin_home' %}" class="logo" title="Wagtail v.{% wagtail_version %}"><img src="{{ STATIC_URL }}wagtailadmin/images/wagtail-logo.svg" alt="Wagtail" width="80" /><span>{% trans "Dashboard" %}</span></a>
<form class="nav-search" action="{% url 'wagtailadmin_pages_search' %}" method="get">
<div>
<label for="menu-search-q">{% trans "Search" %}</label>
<input type="text" id="menu-search-q" name="q" placeholder="{% trans 'Search' %}" />
<button type="submit">{% trans "Search" %}</button>
</div>
</form>
{% main_nav %}
</div>

View file

@ -1,13 +1,8 @@
{% extends "wagtailadmin/edit_handlers/chooser_panel.html" %}
{% load i18n %}
{% comment %}
TODO: make it possible to specify button labels that are better tuned to the
particular use case: e.g. "Choose an author", "Choose a location"
{% endcomment %}
{% block chosen_state_view %}
<span class="title">{{ page.title }}</span>
{% endblock %}
{% block choose_another_button_label %}{% trans "Choose another page" %}{% endblock %}
{% block choose_button_label %}{% trans "Choose a page" %}{% endblock %}
{% block choose_another_button_label %}{{ choose_another_text_str }}{% endblock %}
{% block choose_button_label %}{{ choose_one_text_str }}{% endblock %}

View file

@ -1,6 +1,6 @@
{% load i18n %}
{% trans "Page privacy" as title_str %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="no-view" %}
<div class="nice-padding">
<p>{% trans "This page has been made private by a parent page." %}</p>

View file

@ -1,6 +1,6 @@
{% load i18n %}
{% trans "Page privacy" as title_str %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="no-view" %}
<div class="nice-padding">
<p class="help-block help-warning">{% trans "Privacy changes apply to all children of this page too." %}</p>

View file

@ -0,0 +1,31 @@
{% load i18n wagtailadmin_tags %}
{% if not page_perms %}
{% page_permissions page as page_perms %}
{% endif %}
<div class="lock-indicator {% if page.locked %}locked{% else %}unlocked{% endif %}">
{% trans "Edit lock" %}
{% if page.locked %}
{% if page_perms.can_lock %}
<form action="{% url 'wagtailadmin_pages_unlock' page.id %}" method="POST" class="status-tag primary">
{% csrf_token %}
<input type="hidden" name="next" value="{% url 'wagtailadmin_pages_edit' page.id %}">
<input type="submit" class="unbutton" value="{% trans "Locked" %}">
</form>
{% else %}
<span class="status-tag primary">Locked</span>
{% endif %}
{% else %}
{% if page_perms.can_lock %}
<form action="{% url 'wagtailadmin_pages_lock' page.id %}" method="POST" class="status-tag secondary">
{% csrf_token %}
<input type="hidden" name="next" value="{% url 'wagtailadmin_pages_edit' page.id %}">
<input type="submit" class="unbutton" value="{% trans "Unlocked" %}">
</form>
{% else %}
<span class="status-tag secondary">Unlocked</span>
{% endif %}
{% endif %}
</div>

View file

@ -1,20 +0,0 @@
{% load i18n %}
<div class="wagtail-moderator-controls">
<div class="message">
<p>{% blocktrans with title=revision.page.title submitted_by=revision.user.get_full_name|default:revision.user.username submitted_on=revision.created_at|date:"d M Y H:i" %}
Previewing '{{ title }}', submitted by {{ submitted_by }} on {{ submitted_on }}.
{% endblocktrans %}</p>
</div>
<div class="actions">
<a href="{% url 'wagtailadmin_pages_edit' revision.page.id %}" class="button">{% trans 'Edit' %}</a>
<form action="{% url 'wagtailadmin_pages_approve_moderation' revision.id %}" method="POST">
{% csrf_token %}
<input type="submit" class="button yes" value="{% trans 'Approve' %}" />
</form>
<form action="{% url 'wagtailadmin_pages_reject_moderation' revision.id %}" method="POST">
{% csrf_token %}
<input type="submit" class="button no" value="{% trans 'Reject' %}" />
</form>
</div>
</div>

View file

@ -10,14 +10,14 @@
{% if page_perms.can_set_view_restrictions %}
<a href="{% url 'wagtailadmin_pages_set_privacy' page.id %}" class="status-tag primary action-set-privacy">
{# labels are shown/hidden in CSS according to the 'private' / 'public' class on view-permission-indicator #}
<span class="label-public icon icon-unlocked">{% trans 'Public' %}</span>
<span class="label-private icon icon-locked">{% trans 'Private' %}</span>
<span class="label-public icon icon-view">{% trans 'Public' %}</span>
<span class="label-private icon icon-no-view">{% trans 'Private' %}</span>
</a>
{% else %}
{% if is_public %}
<span class="label-public status-tag primary icon icon-unlocked ">{% trans 'Public' %}</span>
<span class="label-public status-tag primary icon icon-view ">{% trans 'Public' %}</span>
{% else %}
<span class="label-private status-tag primary icon icon-locked">{% trans 'Private' %}</span>
<span class="label-private status-tag primary icon icon-no-view">{% trans 'Private' %}</span>
{% endif %}
{% endif %}
</div>

View file

@ -23,6 +23,7 @@
{% endif %}
{% include "wagtailadmin/pages/_privacy_indicator.html" with page=page page_perms=page_perms only %}
{% include "wagtailadmin/pages/_lock_indicator.html" %}
</div>
</div>
</header>
@ -35,20 +36,23 @@
<ul>
<li class="actions">
<div class="dropdown dropup dropdown-button match-width">
<input type="submit" value="{% trans 'Save draft' %}" class="button" />
<div class="dropdown-toggle icon icon-arrow-up"></div>
<ul role="menu">
{% if page_perms.can_unpublish %}
<li><a href="{% url 'wagtailadmin_pages_unpublish' page.id %}">{% trans 'Unpublish' %}</a></li>
{% endif %}
{% if page_perms.can_delete %}
<li><a href="{% url 'wagtailadmin_pages_delete' page.id %}" class="shortcut">{% trans 'Delete' %}</a></li>
{% endif %}
{% if page_perms.can_publish %}
<li><input type="submit" name="action-publish" value="{% trans 'Publish' %}" class="button" /></li>
{% endif %}
<li><input type="submit" name="action-submit" value="{% trans 'Submit for moderation' %}" class="button" /></li>
</ul>
<input type="submit" value="{% if page.locked %}{% trans 'Page locked' %}{% else %}{% trans 'Save draft' %}{% endif %}" class="button" {% if page.locked %}disabled {% endif %} />
{% if not page.locked %}
<div class="dropdown-toggle icon icon-arrow-up"></div>
<ul role="menu">
{% if page_perms.can_unpublish %}
<li><a href="{% url 'wagtailadmin_pages_unpublish' page.id %}">{% trans 'Unpublish' %}</a></li>
{% endif %}
{% if page_perms.can_delete %}
<li><a href="{% url 'wagtailadmin_pages_delete' page.id %}" class="shortcut">{% trans 'Delete' %}</a></li>
{% endif %}
{% if page_perms.can_publish %}
<li><input type="submit" name="action-publish" value="{% trans 'Publish' %}" class="button" /></li>
{% endif %}
<li><input type="submit" name="action-submit" value="{% trans 'Submit for moderation' %}" class="button" /></li>
</ul>
{% endif %}
</div>
</li>

View file

@ -9,12 +9,12 @@
{% include "wagtailadmin/shared/breadcrumb.html" with page=parent_page %}
</header>
<form id="page-reorder-form">
{% csrf_token %}
{% page_permissions parent_page as parent_page_perms %}
{% include "wagtailadmin/pages/list.html" with sortable=1 allow_navigation=1 full_width=1 parent_page=parent_page orderable=parent_page_perms.can_reorder_children %}
{% include "wagtailadmin/pages/list.html" with sortable=1 allow_navigation=1 full_width=1 parent_page=parent_page orderable=parent_page_perms.can_reorder_children %}
</form>
{% endblock %}
@ -30,9 +30,10 @@
var orderform = $('#page-reorder-form');
$('.listing tbody').sortable({
cursor: "move",
containment: "parent",
handle: ".handle",
cursor: "move",
tolerance: "pointer",
containment: "parent",
handle: ".handle",
items: "> tr",
axis: "y",
placeholder: "dropzone",

View file

@ -40,7 +40,7 @@
{% test_page_is_public parent_page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
<span class="privacy-indicator icon icon-no-view" title="{% trans "This page is protected from public view" %}"></span>
{% endif %}
</h2>
{% elif choosing %}
@ -53,7 +53,7 @@
{% test_page_is_public parent_page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
<span class="privacy-indicator icon icon-no-view" title="This page is protected from public view"></span>
{% endif %}
</h2>
{% else %}
@ -61,7 +61,11 @@
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
<a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}">{{ parent_page.title }}</a>
{% else %}
{{ parent_page.title }}
{{ parent_page.title }}
{% endif %}
{% if parent_page.locked %}
<span class="locked-indicator icon icon-locked" title="{% trans "This page is locked to further editing" %}"></span>
{% endif %}
</h2>
@ -181,7 +185,11 @@
{% test_page_is_public page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
<span class="privacy-indicator icon icon-no-view" title="{% trans 'This page is protected from public view' %}"></span>
{% endif %}
{% if page.locked %}
<span class="locked-indicator icon icon-locked" title="{% trans 'This page is locked to further editing' %}"></span>
{% endif %}
</h2>
{% if not moving and not choosing %}

View file

@ -17,7 +17,7 @@
<ul class="filter-options">
<li><a href="{% url 'wagtailimages_index' %}?q={{ query_string|urlencode }}" class="icon icon-image">{% trans "Images" %}</a></li>
<li><a href="{% url 'wagtaildocs_index' %}?q={{ query_string|urlencode }}" class="icon icon-doc-full-inverse">{% trans "Documents" %}</a></li>
<li><a href="{% url 'wagtailusers_index' %}?q={{ query_string|urlencode }}" class="icon icon-user">{% trans "Users" %}</a></li>
<li><a href="{% url 'wagtailusers_users_index' %}?q={{ query_string|urlencode }}" class="icon icon-user">{% trans "Users" %}</a></li>
</ul>
</nav>
{% endif %}

View file

@ -2,9 +2,7 @@
{% load i18n %}
<nav class="nav-main">
<ul>
{% for menu_item in menu_items %}
{{ menu_item.render_html }}
{% endfor %}
{{ menu_html }}
<li class="footer">
<div class="avatar icon icon-user"><a href="{% url 'wagtailadmin_account' %}" title="{% trans 'Account settings' %}">{% if request.user.email %}<img src="{% gravatar_url request.user.email %}" />{% endif %}</a></div>

View file

@ -2,12 +2,11 @@ from __future__ import unicode_literals
from django.conf import settings
from django import template
from django.forms import Media
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy, PageViewRestriction
from wagtail.wagtailcore.utils import camelcase_to_underscore
from wagtail.wagtailadmin.menu import get_master_menu_item_list
from wagtail.wagtailadmin.menu import admin_menu
register = template.Library()
@ -30,23 +29,15 @@ def explorer_subnav(nodes):
@register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True)
def main_nav(context):
request = context['request']
menu_items = [item for item in get_master_menu_item_list() if item.is_shown(request)]
for fn in hooks.get_hooks('construct_main_menu'):
fn(request, menu_items)
return {
'menu_items': sorted(menu_items, key=lambda i: i.order),
'menu_html': admin_menu.render_html(request),
'request': request,
}
@register.simple_tag
def main_nav_js():
media = Media()
for item in get_master_menu_item_list():
media += item.media
return media['js']
return admin_menu.media['js']
@register.filter("ellipsistrim")

View file

@ -443,6 +443,28 @@ 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_page_edit_post_when_locked(self):
# Tests that trying to edit a locked page results in an error
# Lock the page
self.child_page.locked = True
self.child_page.save()
# Post
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Shouldn't be redirected
self.assertContains(response, "The page could not be saved as it is locked")
# The page shouldn't have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertFalse(child_page_new.has_unpublished_changes)
def test_edit_post_scheduled(self):
# put go_live_at and expire_at several days away from the current date, to avoid
# false matches in content_json__contains tests
@ -511,6 +533,11 @@ class TestPageEdit(TestCase, WagtailTestUtils):
signal_page[0] = instance
page_published.connect(page_published_handler)
# Set has_unpublished_changes=True on the existing record to confirm that the publish action
# is resetting it (and not just leaving it alone)
self.child_page.has_unpublished_changes = True
self.child_page.save()
# Tests publish from edit page
post_data = {
'title': "I've been edited!",
@ -559,6 +586,9 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# 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())
# The page SHOULD have the "has_unpublished_changes" flag set, because the changes are not visible as a live page yet
self.assertTrue(child_page_new.has_unpublished_changes, "A page scheduled for future publishing should have has_unpublished_changes=True")
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)
@ -968,6 +998,7 @@ class TestPageCopy(TestCase, WagtailTestUtils):
title="Hello world!",
slug='hello-world',
live=True,
has_unpublished_changes=False,
))
# Create a couple of child pages
@ -975,12 +1006,14 @@ class TestPageCopy(TestCase, WagtailTestUtils):
title="Child page",
slug='child-page',
live=True,
has_unpublished_changes=True,
))
self.test_unpublished_child_page = self.test_page.add_child(instance=SimplePage(
title="Unpublished Child page",
slug='unpublished-child-page',
live=False,
has_unpublished_changes=True,
))
# Login
@ -1033,6 +1066,7 @@ class TestPageCopy(TestCase, WagtailTestUtils):
# Check that the copy is not live
self.assertFalse(page_copy.live)
self.assertTrue(page_copy.has_unpublished_changes)
# Check that the owner of the page is set correctly
self.assertEqual(page_copy.owner, self.user)
@ -1060,6 +1094,7 @@ class TestPageCopy(TestCase, WagtailTestUtils):
# Check that the copy is not live
self.assertFalse(page_copy.live)
self.assertTrue(page_copy.has_unpublished_changes)
# Check that the owner of the page is set correctly
self.assertEqual(page_copy.owner, self.user)
@ -1072,10 +1107,12 @@ class TestPageCopy(TestCase, WagtailTestUtils):
child_copy = page_copy.get_children().filter(slug='child-page').first()
self.assertNotEqual(child_copy, None)
self.assertFalse(child_copy.live)
self.assertTrue(child_copy.has_unpublished_changes)
unpublished_child_copy = page_copy.get_children().filter(slug='unpublished-child-page').first()
self.assertNotEqual(unpublished_child_copy, None)
self.assertFalse(unpublished_child_copy.live)
self.assertTrue(unpublished_child_copy.has_unpublished_changes)
def test_page_copy_post_copy_subpages_publish_copies(self):
post_data = {
@ -1097,6 +1134,7 @@ class TestPageCopy(TestCase, WagtailTestUtils):
# Check that the copy is live
self.assertTrue(page_copy.live)
self.assertFalse(page_copy.has_unpublished_changes)
# Check that the owner of the page is set correctly
self.assertEqual(page_copy.owner, self.user)
@ -1109,10 +1147,12 @@ class TestPageCopy(TestCase, WagtailTestUtils):
child_copy = page_copy.get_children().filter(slug='child-page').first()
self.assertNotEqual(child_copy, None)
self.assertTrue(child_copy.live)
self.assertTrue(child_copy.has_unpublished_changes)
unpublished_child_copy = page_copy.get_children().filter(slug='unpublished-child-page').first()
self.assertNotEqual(unpublished_child_copy, None)
self.assertFalse(unpublished_child_copy.live)
self.assertTrue(unpublished_child_copy.has_unpublished_changes)
def test_page_copy_post_existing_slug(self):
# This tests the existing slug checking on page copy
@ -1276,9 +1316,7 @@ class TestPageUnpublish(TestCase, WagtailTestUtils):
page_unpublished.connect(page_unpublished_handler)
# Post to the unpublish page
response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )))
# Should be redirected to explorer page
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
@ -1308,6 +1346,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
title="Hello world!",
slug='hello-world',
live=False,
has_unpublished_changes=True,
)
root_page.add_child(instance=self.page)
@ -1327,29 +1366,45 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
page_published.connect(page_published_handler)
# Post
response = 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",
})
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )))
# Check that the user was redirected to the dashboard
self.assertRedirects(response, reverse('wagtailadmin_home'))
page = Page.objects.get(id=self.page.id)
# Page must be live
self.assertTrue(Page.objects.get(id=self.page.id).live)
self.assertTrue(page.live, "Approving moderation failed to set live=True")
# Page should now have no unpublished changes
self.assertFalse(page.has_unpublished_changes, "Approving moderation failed to set has_unpublished_changes=False")
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.page)
self.assertEqual(signal_page[0], signal_page[0].specific)
def test_approve_moderation_when_later_revision_exists(self):
self.page.title = "Goodbye world!"
self.page.save_revision(user=self.submitter, submitted_for_moderation=False)
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )))
# Check that the user was redirected to the dashboard
self.assertRedirects(response, reverse('wagtailadmin_home'))
page = Page.objects.get(id=self.page.id)
# Page must be live
self.assertTrue(page.live, "Approving moderation failed to set live=True")
# Page content should be the submitted version, not the published one
self.assertEqual(page.title, "Hello world!")
# Page should still have unpublished changes
self.assertTrue(page.has_unpublished_changes, "has_unpublished_changes incorrectly cleared on approve_moderation when a later revision exists")
def test_approve_moderation_view_bad_revision_id(self):
"""
This tests that the approve moderation view handles invalid revision ids correctly
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(12345, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(12345, )))
# Check that the user recieved a 404 response
self.assertEqual(response.status_code, 404)
@ -1366,9 +1421,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
self.user.save()
# Post
response = 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",
})
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
@ -1378,9 +1431,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
This posts to the reject moderation view and checks that the page was rejected
"""
# Post
response = 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",
})
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )))
# Check that the user was redirected to the dashboard
self.assertRedirects(response, reverse('wagtailadmin_home'))
@ -1396,9 +1447,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
This tests that the reject moderation view handles invalid revision ids correctly
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(12345, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(12345, )))
# Check that the user recieved a 404 response
self.assertEqual(response.status_code, 404)
@ -1415,9 +1464,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
self.user.save()
# Post
response = 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",
})
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
@ -1594,14 +1641,10 @@ class TestNotificationPreferences(TestCase, WagtailTestUtils):
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",
})
return self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )))
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",
})
return self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )))
def test_vanilla_profile(self):
# Check that the vanilla profile has rejected notifications on
@ -1681,6 +1724,199 @@ class TestNotificationPreferences(TestCase, WagtailTestUtils):
self.assertEqual(len(mail.outbox), 0)
class TestLocking(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
self.user = self.login()
# 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)
def test_lock_post(self):
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )))
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
def test_lock_get(self):
response = self.client.get(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )))
# Check response
self.assertEqual(response.status_code, 405)
# Check that the page is still unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_lock_post_already_locked(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )))
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is still locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
def test_lock_post_with_good_redirect(self):
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )), {
'next': reverse('wagtailadmin_pages_edit', args=(self.child_page.id, ))
})
# Check response
self.assertRedirects(response, reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )))
# Check that the page is locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_with_bad_redirect(self):
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )), {
'next': 'http://www.google.co.uk'
})
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is locked
self.assertTrue(page.objects.get(id=self.child_page.id).locked)
def test_lock_post_bad_page(self):
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(9999, )))
# Check response
self.assertEqual(response.status_code, 404)
# Check that the page is still unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_lock_post_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
response = self.client.post(reverse('wagtailadmin_pages_lock', args=(self.child_page.id, )))
# Check response
self.assertEqual(response.status_code, 403)
# Check that the page is still unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )))
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_get(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.get(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )))
# Check response
self.assertEqual(response.status_code, 405)
# Check that the page is still locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_already_unlocked(self):
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )))
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is still unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_with_good_redirect(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )), {
'next': reverse('wagtailadmin_pages_edit', args=(self.child_page.id, ))
})
# Check response
self.assertRedirects(response, reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )))
# Check that the page is unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_with_bad_redirect(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )), {
'next': 'http://www.google.co.uk'
})
# Check response
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is unlocked
self.assertFalse(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_bad_page(self):
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(9999, )))
# Check response
self.assertEqual(response.status_code, 404)
# Check that the page is still locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
def test_unlock_post_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Lock the page
self.child_page.locked = True
self.child_page.save()
response = self.client.post(reverse('wagtailadmin_pages_unlock', args=(self.child_page.id, )))
# Check response
self.assertEqual(response.status_code, 403)
# Check that the page is still locked
self.assertTrue(Page.objects.get(id=self.child_page.id).locked)
class TestIssue197(TestCase, WagtailTestUtils):
def test_issue_197(self):
# Find root page

View file

@ -204,7 +204,7 @@ class TestPrivacyIndicators(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
# Must have one privacy icon (next to the private page)
self.assertContains(response, "<span class=\"privacy-indicator icon icon-locked\"", count=1)
self.assertContains(response, "<span class=\"privacy-indicator icon icon-no-view\"", count=1)
def test_explorer_list_private(self):
"""
@ -216,7 +216,7 @@ class TestPrivacyIndicators(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
# Must have one privacy icon (next to the private child page)
self.assertContains(response, "<span class=\"privacy-indicator icon icon-locked\"", count=1)
self.assertContains(response, "<span class=\"privacy-indicator icon icon-no-view\"", count=1)
def test_edit_public(self):
"""

View file

@ -73,6 +73,9 @@ urlpatterns += [
url(r'^pages/(\d+)/privacy/$', page_privacy.set_privacy, name='wagtailadmin_pages_set_privacy'),
url(r'^pages/(\d+)/lock/$', pages.lock, name='wagtailadmin_pages_lock'),
url(r'^pages/(\d+)/unlock/$', pages.unlock, name='wagtailadmin_pages_unlock'),
url(r'^choose-page/$', chooser.browse, name='wagtailadmin_choose_page'),
url(r'^choose-page/(\d+)/$', chooser.browse, name='wagtailadmin_choose_page_child'),
url(r'^choose-external-link/$', chooser.external_link, name='wagtailadmin_choose_page_external_link'),

View file

@ -9,7 +9,8 @@ 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.http import require_GET
from django.utils.http import is_safe_url
from django.views.decorators.http import require_GET, require_POST
from django.views.decorators.vary import vary_on_headers
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
@ -18,7 +19,6 @@ from wagtail.wagtailadmin import tasks, signals
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
from wagtail.wagtailcore.signals import page_published, page_unpublished
@permission_required('wagtailadmin.access_admin')
@ -181,39 +181,31 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
form.clean = clean
if form.is_valid():
page = form.save(commit=False) # don't save yet, as we need treebeard to assign tree params
page = form.save(commit=False)
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.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:
# Set live to False and has_unpublished_changes to True if we are not publishing
if not is_publishing:
page.live = False
page.has_unpublished_changes = True
parent_page.add_child(instance=page) # assign tree parameters - will cause page to be saved
# Save page
parent_page.add_child(instance=page)
# Pass approved_go_live_at to save_revision
page.save_revision(
# Save revision
revision = page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
# Publish
if is_publishing:
revision.publish()
# Notifications
if is_publishing:
page_published.send(sender=page_class, instance=page)
messages.success(request, _("Page '{0}' published.").format(page.title))
elif is_submitting:
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
@ -297,55 +289,33 @@ def edit(request, page_id):
return cleaned_data
form.clean = clean
if form.is_valid():
if form.is_valid() and not page.locked:
page = form.save(commit=False)
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
# Save revision
revision = page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
)
# Publish
if is_publishing:
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
# We need save the page this way to workaround a bug
# in django-modelcluster causing m2m fields to not
# be committed to the database. See github issue #192
form.save(commit=False)
page.save()
# Clear approved_go_live_at for older revisions
page.revisions.update(
submitted_for_moderation=False,
approved_go_live_at=None,
)
revision.publish()
else:
# not publishing the page
# Set has_unpublished_changes flag
if page.live:
# To avoid overwriting the live version, we only save the page
# to the revisions table
form.save(commit=False)
Page.objects.filter(id=page.id).update(has_unpublished_changes=True)
else:
page.has_unpublished_changes = True
form.save(commit=False)
page.save()
page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
# Notifications
if is_publishing:
page_published.send(sender=page.__class__, instance=page)
messages.success(request, _("Page '{0}' published.").format(page.title))
elif is_submitting:
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
@ -360,7 +330,10 @@ 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"))
if page.locked:
messages.error(request, _("The page could not be saved as it is locked"))
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 = (
@ -391,19 +364,9 @@ def delete(request, page_id):
raise PermissionDenied
if request.POST:
if page.live:
# fetch params to pass to the page_unpublished_signal, before the
# deletion happens
specific_class = page.specific_class
specific_page = page.specific
parent_id = page.get_parent().id
page.delete()
# If the page is live, send the unpublished signal
if page.live:
page_unpublished.send(sender=specific_class, instance=specific_page)
messages.success(request, _("Page '{0}' deleted.").format(page.title))
for fn in hooks.get_hooks('after_delete_page'):
@ -537,19 +500,12 @@ def unpublish(request, page_id):
if not page.permissions_for_user(request.user).can_unpublish():
raise PermissionDenied
if request.POST:
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)
page_unpublished.send(sender=page.specific_class, instance=page.specific)
if request.method == 'POST':
page.unpublish()
messages.success(request, _("Page '{0}' unpublished.").format(page.title))
return redirect('wagtailadmin_explore', parent_id)
return redirect('wagtailadmin_explore', page.get_parent().id)
return render(request, 'wagtailadmin/pages/confirm_unpublish.html', {
'page': page,
@ -634,8 +590,13 @@ def set_page_position(request, page_to_move_id):
# so don't bother to catch InvalidMoveToDescendant
if position_page:
# Move page into this position
page_to_move.move(position_page, pos='left')
# If the page has been moved to the right, insert it to the
# right. If left, then left.
old_position = list(parent_page.get_children()).index(page_to_move)
if int(position) < old_position:
page_to_move.move(position_page, pos='left')
elif int(position) > old_position:
page_to_move.move(position_page, pos='right')
else:
# Move page to end
page_to_move.move(parent_page, pos='last-child')
@ -674,7 +635,7 @@ def copy(request, page_id):
# Unpublish copied pages if we need to
if not publish_copies:
new_page.get_descendants(inclusive=True).update(live=False)
new_page.get_descendants(inclusive=True).unpublish()
# Assign user of this request as the owner of all the new pages
new_page.get_descendants(inclusive=True).update(owner=request.user)
@ -760,9 +721,8 @@ def approve_moderation(request, revision_id):
messages.error(request, _("The page '{0}' is not currently awaiting moderation.").format(revision.page.title))
return redirect('wagtailadmin_home')
if request.POST:
revision.publish()
page_published.send(sender=revision.page.__class__, instance=revision.page.specific)
if request.method == 'POST':
revision.approve_moderation()
messages.success(request, _("Page '{0}' published.").format(revision.page.title))
tasks.send_notification.delay(revision.id, 'approved', request.user.id)
@ -779,9 +739,8 @@ def reject_moderation(request, revision_id):
messages.error(request, _("The page '{0}' is not currently awaiting moderation.").format( revision.page.title))
return redirect('wagtailadmin_home')
if request.POST:
revision.submitted_for_moderation = False
revision.save(update_fields=['submitted_for_moderation'])
if request.method == 'POST':
revision.reject_moderation()
messages.success(request, _("Page '{0}' rejected for publication.").format(revision.page.title))
tasks.send_notification.delay(revision.id, 'rejected', request.user.id)
@ -806,3 +765,49 @@ def preview_for_moderation(request, revision_id):
# pass in the real user request rather than page.dummy_request(), so that request.user
# and request.revision_id will be picked up by the wagtail user bar
return page.serve_preview(request, page.default_preview_mode)
@permission_required('wagtailadmin.access_admin')
@require_POST
def lock(request, page_id):
# Get the page
page = get_object_or_404(Page, id=page_id)
# Check permissions
if not page.permissions_for_user(request.user).can_lock():
raise PermissionDenied
# Lock the page
if not page.locked:
page.locked = True
page.save()
# Redirect
redirect_to = request.POST.get('next', None)
if redirect_to and is_safe_url(url=redirect_to, host=request.get_host()):
return redirect(redirect_to)
else:
return redirect('wagtailadmin_explore', page.get_parent().id)
@permission_required('wagtailadmin.access_admin')
@require_POST
def unlock(request, page_id):
# Get the page
page = get_object_or_404(Page, id=page_id)
# Check permissions
if not page.permissions_for_user(request.user).can_lock():
raise PermissionDenied
# Unlock the page
if page.locked:
page.locked = False
page.save()
# Redirect
redirect_to = request.POST.get('next', None)
if redirect_to and is_safe_url(url=redirect_to, host=request.get_host()):
return redirect(redirect_to)
else:
return redirect('wagtailadmin_explore', page.get_parent().id)

View file

@ -1,14 +1,16 @@
from django.core import urlresolvers
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailadmin.menu import MenuItem, SubmenuMenuItem, settings_menu
class ExplorerMenuItem(MenuItem):
class Media:
js = ['wagtailadmin/js/explorer-menu.js']
@hooks.register('register_admin_menu_item')
def register_explorer_menu_item():
return ExplorerMenuItem(
@ -17,8 +19,13 @@ def register_explorer_menu_item():
attrs={'data-explorer-menu-url': urlresolvers.reverse('wagtailadmin_explorer_nav')},
order=100)
@hooks.register('register_admin_menu_item')
def register_search_menu_item():
return MenuItem(
_('Search'), urlresolvers.reverse('wagtailadmin_pages_search'),
classnames='icon icon-search', order=200)
def register_settings_menu():
return SubmenuMenuItem(
_('Settings'), settings_menu, classnames='icon icon-cogs', order=10000)
@hooks.register('register_permissions')
def register_permissions():
return Permission.objects.filter(content_type__app_label='wagtailadmin', codename='access_admin')

View file

@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand
from django.utils import dateparse, timezone
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published, page_unpublished
def revision_date_expired(r):
@ -56,15 +55,11 @@ class Command(BaseCommand):
else:
print("No expired pages to be deactivated found.")
else:
# need to get the list of expired pages before the update,
# so that we can fire the page_unpublished signal on them afterwards
expired_pages_list = list(expired_pages)
expired_pages.update(expired=True, live=False)
# Fire page_unpublished signal for all expired pages
for page in expired_pages_list:
page_unpublished.send(sender=page.specific_class, instance=page.specific)
# Unpublish the expired pages
# Cast to list to make sure the query is fully evaluated
# before unpublishing anything
for page in list(expired_pages):
page.unpublish(set_expired=True)
# 2. get all page revisions for moderation that have been expired
expired_revs = [
@ -118,6 +113,3 @@ class Command(BaseCommand):
# just run publish for the revision -- since the approved go
# live datetime is before now it will make the page live
rp.publish()
# Fire page_published signal
page_published.send(sender=rp.page.specific_class, instance=rp.page.specific)

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0002_initial_data'),
]
operations = [
migrations.AlterField(
model_name='grouppagepermission',
name='permission_type',
field=models.CharField(max_length=20, choices=[(b'add', b'Add/edit pages you own'), (b'edit', b'Add/edit any page'), (b'publish', b'Publish any page')]),
),
migrations.AlterUniqueTogether(
name='grouppagepermission',
unique_together=set([('group', 'page', 'permission_type')]),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0003_add_uniqueness_constraint_on_group_page_permission'),
]
operations = [
migrations.AddField(
model_name='page',
name='locked',
field=models.BooleanField(default=False, editable=False),
preserve_default=True,
),
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
def add_page_lock_permission_to_moderators(apps, schema_editor):
Group = apps.get_model('auth.Group')
Page = apps.get_model('wagtailcore.Page')
GroupPagePermission = apps.get_model('wagtailcore.GroupPagePermission')
root_pages = Page.objects.filter(depth=1)
try:
moderators_group = Group.objects.get(name='Moderators')
for page in root_pages:
GroupPagePermission.objects.create(
group=moderators_group, page=page, permission_type='lock')
except Group.DoesNotExist:
pass
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0004_page_locked'),
]
operations = [
migrations.RunPython(add_page_lock_permission_to_moderators),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0005_add_page_lock_permission_to_moderators'),
]
operations = [
migrations.AlterField(
model_name='grouppagepermission',
name='permission_type',
field=models.CharField(max_length=20, choices=[(b'add', b'Add/edit pages you own'), (b'edit', b'Add/edit any page'), (b'publish', b'Publish any page'), (b'lock', b'Lock/unlock any page')]),
),
]

View file

@ -9,6 +9,8 @@ from modelcluster.models import ClusterableModel, get_all_child_relations
from django.db import models, connection, transaction
from django.db.models import Q
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
from django.http import Http404
from django.core.cache import cache
from django.core.handlers.wsgi import WSGIRequest
@ -29,6 +31,7 @@ from treebeard.mp_tree import MP_Node
from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_string
from wagtail.wagtailcore.query import PageQuerySet
from wagtail.wagtailcore.url_routing import RouteResult
from wagtail.wagtailcore.signals import page_published, page_unpublished
from wagtail.wagtailsearch import index
from wagtail.wagtailsearch.backends import get_search_backend
@ -69,14 +72,14 @@ class Site(models.Model):
still be routed to a different hostname which is set as the default
"""
try:
hostname = request.META['HTTP_HOST'].split(':')[0] # KeyError here goes to the final except clause
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
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
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.
@ -106,9 +109,9 @@ class Site(models.Model):
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 }
]}
)
% {'hostname': default.hostname}
]}
)
# clear the wagtail_site_root_paths cache whenever Site records are updated
def save(self, *args, **kwargs):
@ -267,14 +270,17 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True)
expired = models.BooleanField(default=False, editable=False)
locked = models.BooleanField(default=False, editable=False)
search_fields = (
index.SearchField('title', partial_match=True, boost=100),
index.SearchField('title', partial_match=True, boost=2),
index.FilterField('id'),
index.FilterField('live'),
index.FilterField('owner'),
index.FilterField('content_type'),
index.FilterField('path'),
index.FilterField('depth'),
index.FilterField('locked'),
)
def __init__(self, *args, **kwargs):
@ -355,8 +361,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
SET url_path = %s || substring(url_path from %s)
WHERE path LIKE %s AND id <> %s
"""
cursor.execute(update_statement,
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
cursor.execute(update_statement, [new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
#: Return this page in its most specific subclassed form.
@cached_property
@ -423,6 +428,21 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
else:
return self.specific
def unpublish(self, set_expired=False, commit=True):
if self.live:
self.live = False
self.has_unpublished_changes = True
if set_expired:
self.expired = True
if commit:
self.save()
page_unpublished.send(sender=self.specific_class, instance=self.specific)
self.revisions.update(approved_go_live_at=None)
def get_context(self, request, *args, **kwargs):
return {
'self': self,
@ -826,7 +846,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
def get_navigation_menu_items():
# Get all pages that appear in the navigation menu: ones which have children,
# or are at the top-level (this rule required so that an empty site out-of-the-box has a working menu)
pages = Page.objects.filter(Q(depth=2)|Q(numchild__gt=0)).order_by('path')
pages = Page.objects.filter(Q(depth=2) | Q(numchild__gt=0)).order_by('path')
# Turn this into a tree structure:
# tree_node = (page, children)
@ -864,6 +884,14 @@ def get_navigation_menu_items():
return []
@receiver(pre_delete, sender=Page)
def unpublish_page_before_delete(sender, instance, **kwargs):
# Make sure pages are unpublished before deleting
if instance.live:
# Don't bother to save, this page is just about to be deleted!
instance.unpublish(commit=False)
class Orderable(models.Model):
sort_order = models.IntegerField(null=True, blank=True, editable=False)
sort_order_field = 'sort_order'
@ -914,14 +942,33 @@ class PageRevision(models.Model):
obj.live = self.page.live
obj.has_unpublished_changes = self.page.has_unpublished_changes
obj.owner = self.page.owner
obj.locked = self.page.locked
return obj
def approve_moderation(self):
if self.submitted_for_moderation:
self.publish()
def reject_moderation(self):
if self.submitted_for_moderation:
self.submitted_for_moderation = False
self.save(update_fields=['submitted_for_moderation'])
def is_latest_revision(self):
if self.id is None:
# special case: a revision without an ID is presumed to be newly-created and is thus
# newer than any revision that might exist in the database
return True
latest_revision = PageRevision.objects.filter(page_id=self.page_id).order_by('-created_at').first()
return (latest_revision == self)
def publish(self):
page = self.as_page_object()
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
page.has_unpublished_changes = True
# Instead set the approved_go_live_at of this revision
self.approved_go_live_at = page.go_live_at
self.save()
@ -929,6 +976,8 @@ class PageRevision(models.Model):
page.revisions.exclude(id=self.id).update(approved_go_live_at=None)
else:
page.live = True
# at this point, the page has unpublished changes iff there are newer revisions than this one
page.has_unpublished_changes = not self.is_latest_revision()
# 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
@ -936,14 +985,18 @@ class PageRevision(models.Model):
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)
if page.live:
page_published.send(sender=page.specific_class, instance=page.specific)
def __str__(self):
return '"' + unicode(self.page) + '" at ' + unicode(self.created_at)
PAGE_PERMISSION_TYPE_CHOICES = [
('add', 'Add'),
('edit', 'Edit'),
('publish', 'Publish'),
('add', 'Add/edit pages you own'),
('edit', 'Add/edit any page'),
('publish', 'Publish any page'),
('lock', 'Lock/unlock any page'),
]
@ -952,6 +1005,9 @@ class GroupPagePermission(models.Model):
page = models.ForeignKey('Page', related_name='group_permissions')
permission_type = models.CharField(max_length=20, choices=PAGE_PERMISSION_TYPE_CHOICES)
class Meta:
unique_together = ('group', 'page', 'permission_type')
class UserPagePermissionsProxy(object):
"""Helper object that encapsulates all the page permission rules that this user has
@ -1043,7 +1099,7 @@ class PagePermissionTester(object):
self.user = user_perms.user
self.user_perms = user_perms
self.page = page
self.page_is_root = page.depth == 1 # Equivalent to page.is_root()
self.page_is_root = page.depth == 1 # Equivalent to page.is_root()
if self.user.is_active and not self.user.is_superuser:
self.permissions = set(
@ -1109,6 +1165,9 @@ class PagePermissionTester(object):
def can_set_view_restrictions(self):
return self.can_publish()
def can_lock(self):
return self.user.is_superuser or ('lock' in self.permissions)
def can_publish_subpage(self):
"""
Niggly special case for creating and publishing a page in one go.

View file

@ -1,4 +1,4 @@
from django.db.models import Q
from django.db.models import Q, get_models
from django.contrib.contenttypes.models import ContentType
from treebeard.mp_tree import MP_NodeQuerySet
@ -152,13 +152,17 @@ class PageQuerySet(MP_NodeQuerySet):
"""
return self.exclude(self.sibling_of_q(other, inclusive))
def type_q(self, model):
content_type = ContentType.objects.get_for_model(model)
return Q(content_type=content_type)
def type_q(self, klass):
content_types = ContentType.objects.get_for_models(*[
model for model in get_models()
if issubclass(model, klass)
]).values()
return Q(content_type__in=content_types)
def type(self, model):
"""
This filters the queryset to only contain pages that are an instance of the specified model.
This filters the queryset to only contain pages that are an instance of the specified model (including subclasses).
"""
return self.filter(self.type_q(model))
@ -194,3 +198,9 @@ class PageQuerySet(MP_NodeQuerySet):
"""
search_backend = get_search_backend(backend)
return search_backend.search(query_string, self, fields=None)
def unpublish(self):
"""
This unpublishes all pages in the queryset
"""
self.update(live=False, has_unpublished_changes=True)

View file

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
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 unique constraint on 'GroupPagePermission', fields ['group', 'page', 'permission_type']
db.create_unique('wagtailcore_grouppagepermission', ['group_id', 'page_id', 'permission_type'])
def backwards(self, orm):
# Removing unique constraint on 'GroupPagePermission', fields ['group', 'page', 'permission_type']
db.delete_unique('wagtailcore_grouppagepermission', ['group_id', 'page_id', 'permission_type'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'}),
'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'})
},
'wagtailcore.grouppagepermission': {
'Meta': {'unique_together': "(('group', 'page', 'permission_type'),)", 'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "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'}),
'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': "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'})
},
'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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
'wagtailcore.pageviewrestriction': {
'Meta': {'object_name': 'PageViewRestriction'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'view_restrictions'", 'to': "orm['wagtailcore.Page']"}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'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': "orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
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 'Page.locked'
db.add_column('wagtailcore_page', 'locked',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Page.locked'
db.delete_column('wagtailcore_page', 'locked')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'}),
'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'})
},
'wagtailcore.grouppagepermission': {
'Meta': {'unique_together': "(('group', 'page', 'permission_type'),)", 'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "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'})
},
'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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
'wagtailcore.pageviewrestriction': {
'Meta': {'object_name': 'PageViewRestriction'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'view_restrictions'", 'to': "orm['wagtailcore.Page']"}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'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': "orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models, connection
from django.db.transaction import set_autocommit
class Migration(DataMigration):
def forwards(self, orm):
if connection.vendor == 'sqlite':
set_autocommit(True)
root_pages = orm['wagtailcore.page'].objects.filter(depth=1)
try:
moderators_group = orm['auth.group'].objects.get(name='Moderators')
for page in root_pages:
orm['wagtailcore.grouppagepermission'].objects.create(
group=moderators_group, page=page, permission_type='lock')
except orm['auth.group'].DoesNotExist:
pass
def backwards(self, orm):
pass
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'}),
'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'})
},
'wagtailcore.grouppagepermission': {
'Meta': {'unique_together': "(('group', 'page', 'permission_type'),)", 'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "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'})
},
'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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
'wagtailcore.pageviewrestriction': {
'Meta': {'object_name': 'PageViewRestriction'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'view_restrictions'", 'to': "orm['wagtailcore.Page']"}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'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': "orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']
symmetrical = True

View file

@ -118,6 +118,7 @@ class TestPublishScheduledPagesCommand(TestCase):
title="Hello world!",
slug="hello-world",
live=False,
has_unpublished_changes=True,
go_live_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
@ -132,6 +133,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertFalse(p.has_unpublished_changes)
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
# Check that the page_published signal was fired
@ -139,6 +141,28 @@ class TestPublishScheduledPagesCommand(TestCase):
self.assertEqual(signal_page[0], page)
self.assertEqual(signal_page[0], signal_page[0].specific)
def test_go_live_when_newer_revision_exists(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
has_unpublished_changes=True,
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))
page.title = "Goodbye world!"
page.save_revision(submitted_for_moderation=False)
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertTrue(p.has_unpublished_changes)
self.assertEqual(p.title, "Hello world!")
def test_future_go_live_page_will_not_be_published(self):
page = SimplePage(
title="Hello world!",
@ -174,6 +198,7 @@ class TestPublishScheduledPagesCommand(TestCase):
title="Hello world!",
slug="hello-world",
live=True,
has_unpublished_changes=False,
expire_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
@ -185,6 +210,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(p.has_unpublished_changes)
self.assertTrue(p.expired)
# Check that the page_published signal was fired

View file

@ -299,3 +299,35 @@ class TestPagePermission(TestCase):
self.assertFalse(publishable_pages.filter(id=someone_elses_event_page.id).exists())
self.assertFalse(can_publish_pages)
def test_lock_page_for_superuser(self):
user = get_user_model().objects.get(username='superuser')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
perms = UserPagePermissionsProxy(user).for_page(christmas_page)
self.assertTrue(perms.can_lock())
def test_lock_page_for_moderator(self):
user = get_user_model().objects.get(username='eventmoderator')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
perms = UserPagePermissionsProxy(user).for_page(christmas_page)
self.assertTrue(perms.can_lock())
def test_lock_page_for_editor(self):
user = get_user_model().objects.get(username='eventeditor')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
perms = UserPagePermissionsProxy(user).for_page(christmas_page)
self.assertFalse(perms.can_lock())
def test_lock_page_for_non_editing_user(self):
user = get_user_model().objects.get(username='admin_only_user')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
perms = UserPagePermissionsProxy(user).for_page(christmas_page)
self.assertFalse(perms.can_lock())

View file

@ -260,6 +260,19 @@ class TestPageQuerySet(TestCase):
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_type_includes_subclasses(self):
from wagtail.wagtailforms.models import AbstractEmailForm
pages = Page.objects.type(AbstractEmailForm)
# Check that all objects are instances of AbstractEmailForm
for page in pages:
self.assertIsInstance(page.specific, AbstractEmailForm)
# Check that the contact form page is in the results
contact_us = Page.objects.get(url_path='/home/contact-us/')
self.assertTrue(pages.filter(id=contact_us.id).exists())
def test_not_type(self):
pages = Page.objects.not_type(EventPage)

View file

@ -8,6 +8,7 @@ class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ('title', 'file', 'tags')
widgets = {
'file': forms.FileInput()
}

View file

@ -3,6 +3,8 @@ from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
@ -44,3 +46,10 @@ def editor_js():
""",
urlresolvers.reverse('wagtaildocs_chooser')
)
@hooks.register('register_permissions')
def register_permissions():
document_content_type = ContentType.objects.get(app_label='wagtaildocs', model='document')
document_permissions = Permission.objects.filter(content_type = document_content_type)
return document_permissions

View file

@ -1,7 +1,7 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-snippets{% endblock %}
{% block bodyclass %}menu-forms{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/shared/datetimepicker_translations.html" %}

View file

@ -1,6 +1,9 @@
from __future__ import division
from django.conf import settings
from wagtail.wagtailimages.utils import crop
from wagtail.wagtailimages.utils.rect import Rect
from wagtail.wagtailimages.utils.focal_point import FocalPoint
class BaseImageBackend(object):
@ -35,27 +38,6 @@ class BaseImageBackend(object):
def crop(self, image, crop_box):
raise NotImplementedError('subclasses of BaseImageBackend must provide a crop() method')
def crop_to_centre(self, image, size):
crop_box = crop.crop_to_centre(image.size, size)
if crop_box.size != image.size:
return self.crop(image, crop_box)
else:
return image
def crop_to_point(self, image, size, focal_point):
crop_box = crop.crop_to_point(image.size, size, focal_point)
# Don't crop if we don't need to
if crop_box.size != image.size:
image = self.crop(image, crop_box)
# If the focal points are too large, the cropping system may not
# crop it fully, resize the image if this has happened:
if crop_box.size != size:
image = self.resize_to_fill(image, size)
return image
def resize_to_max(self, image, size, focal_point=None):
"""
Resize image down to fit within the given dimensions, preserving aspect ratio.
@ -68,9 +50,9 @@ class BaseImageBackend(object):
return image
# scale factor if we were to downsize the image to fit the target width
horz_scale = float(target_width) / original_width
horz_scale = target_width / original_width
# scale factor if we were to downsize the image to fit the target height
vert_scale = float(target_height) / original_height
vert_scale = target_height / original_height
# choose whichever of these gives a smaller image
if horz_scale < vert_scale:
@ -92,9 +74,9 @@ class BaseImageBackend(object):
return image
# scale factor if we were to downsize the image to fit the target width
horz_scale = float(target_width) / original_width
horz_scale = target_width / original_width
# scale factor if we were to downsize the image to fit the target height
vert_scale = float(target_height) / original_height
vert_scale = target_height / original_height
# choose whichever of these gives a larger image
if horz_scale > vert_scale:
@ -114,7 +96,7 @@ class BaseImageBackend(object):
if original_width <= target_width:
return image
scale = float(target_width) / original_width
scale = target_width / original_width
final_size = (target_width, int(original_height * scale))
@ -130,23 +112,139 @@ class BaseImageBackend(object):
if original_height <= target_height:
return image
scale = float(target_height) / original_height
scale = target_height / original_height
final_size = (int(original_width * scale), target_height)
return self.resize(image, final_size)
def resize_to_fill(self, image, size, focal_point=None):
def resize_to_fill(self, image, arg, focal_point=None):
"""
Resize down and crop image to fill the given dimensions. Most suitable for thumbnails.
(The final image will match the requested size, unless one or the other dimension is
already smaller than the target size)
"""
if focal_point is not None:
return self.crop_to_point(image, size, focal_point)
size = arg[:2]
# Get crop closeness if it's set
if len(arg) > 2 and arg[2] is not None:
crop_closeness = arg[2] / 100
# Clamp it
if crop_closeness > 1:
crop_closeness = 1
else:
resized_image = self.resize_to_min(image, size)
return self.crop_to_centre(resized_image, size)
crop_closeness = 0
# Get image width and height
(im_width, im_height) = image.size
# Get filter width and height
fl_width = size[0]
fl_height = size[1]
# Get crop aspect ratio
crop_aspect_ratio = fl_width / fl_height
# Get crop max
crop_max_scale = min(im_width, im_height * crop_aspect_ratio)
crop_max_width = crop_max_scale
crop_max_height = crop_max_scale / crop_aspect_ratio
# Initialise crop width and height to max
crop_width = crop_max_width
crop_height = crop_max_height
# Use crop closeness to zoom in
if focal_point is not None:
fp_width = focal_point.width
fp_height = focal_point.height
# Get crop min
crop_min_scale = max(fp_width, fp_height * crop_aspect_ratio)
crop_min_width = crop_min_scale
crop_min_height = crop_min_scale / crop_aspect_ratio
# Sometimes, the focal point may be bigger than the image...
if not crop_min_scale > crop_max_scale:
# Calculate max crop closeness to prevent upscaling
max_crop_closeness = max(
1 - (fl_width - crop_min_width) / (crop_max_width - crop_min_width),
1 - (fl_height - crop_min_height) / (crop_max_height - crop_min_height)
)
# Apply max crop closeness
crop_closeness = min(crop_closeness, max_crop_closeness)
if 1 >= crop_closeness >= 0:
# Get crop width and height
crop_width = crop_max_width + (crop_min_width - crop_max_width) * crop_closeness
crop_height = crop_max_height + (crop_min_height - crop_max_height) * crop_closeness
# Find focal point UV
if focal_point is not None:
fp_x = focal_point.x
fp_y = focal_point.y
else:
# Fall back to positioning in the centre
fp_x = im_width / 2
fp_y = im_height / 2
fp_u = fp_x / im_width
fp_v = fp_y / im_height
# Position crop box based on focal point UV
crop_x = fp_x - (fp_u - 0.5) * crop_width
crop_y = fp_y - (fp_v - 0.5) * crop_height
# Convert crop box into rect
left = crop_x - crop_width / 2
top = crop_y - crop_height / 2
right = crop_x + crop_width / 2
bottom = crop_y + crop_height / 2
# Make sure the entire focal point is in the crop box
if focal_point is not None:
focal_point_left = focal_point.x - focal_point.width / 2
focal_point_top = focal_point.y - focal_point.height / 2
focal_point_right = focal_point.x + focal_point.width / 2
focal_point_bottom = focal_point.y + focal_point.height / 2
if left > focal_point_left:
right -= left - focal_point_left
left = focal_point_left
if top > focal_point_top:
bottom -= top - focal_point_top
top = focal_point_top
if right < focal_point_right:
left += focal_point_right - right
right = focal_point_right
if bottom < focal_point_bottom:
top += focal_point_bottom - bottom
bottom = focal_point_bottom
# Don't allow the crop box to go over the image boundary
if left < 0:
right -= left
left = 0
if top < 0:
bottom -= top
top = 0
if right > im_width:
left -= right - im_width
right = im_width
if bottom > im_height:
top -= bottom - im_height
bottom = im_height
# Crop!
return self.resize_to_min(self.crop(image, Rect(left, top, right, bottom)), size)
def no_operation(self, image, param, focal_point=None):
"""Return the image unchanged"""

View file

@ -21,8 +21,8 @@ class PillowBackend(BaseImageBackend):
image = image.convert('RGB')
return image.resize(size, PIL.Image.ANTIALIAS)
def crop(self, image, crop_box):
return image.crop(crop_box)
def crop(self, image, rect):
return image.crop(rect)
def image_data_as_rgb(self, image):
# https://github.com/thumbor/thumbor/blob/f52360dc96eedd9fc914fcf19eaf2358f7e2480c/thumbor/engines/pil.py#L206-L215

View file

@ -25,10 +25,10 @@ class WandBackend(BaseImageBackend):
new_image.resize(size[0], size[1])
return new_image
def crop(self, image, crop_box):
def crop(self, image, rect):
new_image = image.clone()
new_image.crop(
left=crop_box[0], top=crop_box[1], right=crop_box[2], bottom=crop_box[3]
left=rect[0], top=rect[1], right=rect[2], bottom=rect[3]
)
return new_image

View file

@ -57,3 +57,4 @@ class URLGeneratorForm(forms.Form):
)
width = forms.IntegerField(_("Width"), min_value=0)
height = forms.IntegerField(_("Height"), min_value=0)
closeness = forms.IntegerField(_("Closeness"), min_value=0, initial=0)

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
"""
When the initial migration was created, the focal point fields on image
did not have blank=True set.
This migration fixes this.
"""
dependencies = [
('wagtailimages', '0002_initial_data'),
]
operations = [
migrations.AlterField(
model_name='image',
name='focal_point_height',
field=models.PositiveIntegerField(null=True, blank=True),
),
migrations.AlterField(
model_name='image',
name='focal_point_width',
field=models.PositiveIntegerField(null=True, blank=True),
),
migrations.AlterField(
model_name='image',
name='focal_point_x',
field=models.PositiveIntegerField(null=True, blank=True),
),
migrations.AlterField(
model_name='image',
name='focal_point_y',
field=models.PositiveIntegerField(null=True, blank=True),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailimages', '0003_fix_focal_point_fields'),
]
operations = [
migrations.AlterField(
model_name='rendition',
name='focal_point_key',
field=models.CharField(blank=True, default='', max_length=255, editable=False),
),
]

View file

@ -143,7 +143,7 @@ class AbstractImage(models.Model, TagSearchable):
else:
rendition = self.renditions.get(
filter=filter,
focal_point_key=None,
focal_point_key='',
)
except ObjectDoesNotExist:
file_field = self.file
@ -175,7 +175,6 @@ class AbstractImage(models.Model, TagSearchable):
else:
rendition, created = self.renditions.get_or_create(
filter=filter,
focal_point_key=None,
defaults={'file': generated_image_file}
)
@ -279,6 +278,7 @@ class Filter(models.Model):
# 'original'
# 'width-200'
# 'max-320x200'
# 'fill-200x200-c50'
if self.spec == 'original':
return Filter.OPERATION_NAMES['original'], None
@ -287,6 +287,13 @@ class Filter(models.Model):
if match:
return Filter.OPERATION_NAMES[match.group(1)], int(match.group(2))
match = re.match(r'(fill)-(\d+)x(\d+)-c(\d+)$', self.spec)
if match:
width = int(match.group(2))
height = int(match.group(3))
crop_closeness = int(match.group(4))
return Filter.OPERATION_NAMES[match.group(1)], (width, height, crop_closeness)
match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', self.spec)
if match:
width = int(match.group(2))
@ -345,7 +352,7 @@ class AbstractRendition(models.Model):
file = models.ImageField(upload_to='images', width_field='width', height_field='height')
width = models.IntegerField(editable=False)
height = models.IntegerField(editable=False)
focal_point_key = models.CharField(max_length=255, null=True, editable=False)
focal_point_key = models.CharField(max_length=255, blank=True, default='', editable=False)
@property
def url(self):

View file

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
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):
# Changing field 'Rendition.focal_point_key'
db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, default=''))
def backwards(self, orm):
# Changing field 'Rendition.focal_point_key'
db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
'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': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'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'}),
'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'})
},
'wagtailimages.filter': {
'Meta': {'object_name': 'Filter'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'spec': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
},
'wagtailimages.image': {
'Meta': {'object_name': 'Image'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'focal_point_height': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_width': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_x': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_y': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'height': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'uploaded_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'width': ('django.db.models.fields.IntegerField', [], {})
},
'wagtailimages.rendition': {
'Meta': {'unique_together': "(('image', 'filter', 'focal_point_key'),)", 'object_name': 'Rendition'},
'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['wagtailimages.Filter']"}),
'focal_point_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'height': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'renditions'", 'to': "orm['wagtailimages.Image']"}),
'width': ('django.db.models.fields.IntegerField', [], {})
}
}
complete_apps = ['wagtailimages']

View file

@ -7,6 +7,7 @@ $(function() {
var $filterMethodField = $form.find('select#id_filter_method');
var $widthField = $form.find('input#id_width');
var $heightField = $form.find('input#id_height');
var $closenessField = $form.find('input#id_closeness');
var $result = $this.find('#result-url');
var $loadingMask = $this.find('.loading-mask')
var $preview = $this.find('img.preview');
@ -22,18 +23,28 @@ $(function() {
if (filterSpec == 'original') {
$widthField.prop('disabled', true);
$heightField.prop('disabled', true);
$closenessField.prop('disabled', true);
} else if (filterSpec == 'width') {
$widthField.prop('disabled', false);
$heightField.prop('disabled', true);
$closenessField.prop('disabled', true);
filterSpec += '-' + $widthField.val();
} else if (filterSpec == 'height') {
$widthField.prop('disabled', true);
$heightField.prop('disabled', false);
$closenessField.prop('disabled', true);
filterSpec += '-' + $heightField.val();
} else if (filterSpec == 'min' || filterSpec == 'max' || filterSpec == 'fill') {
$widthField.prop('disabled', false);
$heightField.prop('disabled', false);
filterSpec += '-' + $widthField.val() + 'x' + $heightField.val();
if (filterSpec == 'fill') {
$closenessField.prop('disabled', false);
filterSpec += '-' + $widthField.val() + 'x' + $heightField.val() + '-c' + $closenessField.val()
} else {
$closenessField.prop('disabled', true);
filterSpec += '-' + $widthField.val() + 'x' + $heightField.val();
}
}
// Display note about scaled down images if image is large

View file

@ -38,7 +38,11 @@
{% csrf_token %}
<ul class="fields">
{% for field in uploadform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% if field.is_hidden %}
{{ field }}
{% else %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endif %}
{% endfor %}
<li><input type="submit" value="{% trans 'Upload' %}" /></li>
</ul>

View file

@ -17,6 +17,7 @@
<ul class="field-row">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.width li_classes="field-col col4" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.height li_classes="field-col col4" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.closeness li_classes="field-col col4" %}
</ul>
</li>
</ul>

View file

@ -14,9 +14,10 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.utils import IntegrityError
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.models import get_image_model, Rendition
from wagtail.wagtailimages.formats import (
Format,
get_image_format,
@ -25,8 +26,6 @@ from wagtail.wagtailimages.formats import (
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.backends.pillow import PillowBackend
from wagtail.wagtailimages.utils.crop import crop_to_point, CropBox
from wagtail.wagtailimages.utils.focal_point import FocalPoint
from wagtail.wagtailimages.utils.crypto import generate_signature, verify_signature
from wagtail.tests.models import EventPage, EventPageCarouselItem
from wagtail.wagtailcore.models import Page
@ -950,70 +949,6 @@ class TestGenerateURLView(TestCase, WagtailTestUtils):
}))
class TestCropToPoint(TestCase):
def test_basic(self):
"Test basic cropping in the centre of the image"
self.assertEqual(
crop_to_point((640, 480), (100, 100), FocalPoint(x=320, y=240)),
CropBox(270, 190, 370, 290),
)
def test_basic_no_focal_point(self):
"If focal point is None, it should make one in the centre of the image"
self.assertEqual(
crop_to_point((640, 480), (100, 100), None),
CropBox(270, 190, 370, 290),
)
def test_doesnt_exit_top_left(self):
"Test that the cropbox doesn't exit the image at the top left"
self.assertEqual(
crop_to_point((640, 480), (100, 100), FocalPoint(x=0, y=0)),
CropBox(0, 0, 100, 100),
)
def test_doesnt_exit_bottom_right(self):
"Test that the cropbox doesn't exit the image at the bottom right"
self.assertEqual(
crop_to_point((640, 480), (100, 100), FocalPoint(x=640, y=480)),
CropBox(540, 380, 640, 480),
)
def test_doesnt_get_smaller_than_focal_point(self):
"Test that the cropbox doesn't get any smaller than the focal point"
self.assertEqual(
crop_to_point((640, 480), (10, 10), FocalPoint(x=320, y=240, width=100, height=100)),
CropBox(270, 190, 370, 290),
)
def test_keeps_composition(self):
"Test that the cropbox tries to keep the composition of the original image as much as it can"
self.assertEqual(
crop_to_point((300, 300), (150, 150), FocalPoint(x=100, y=200)),
CropBox(50, 100, 200, 250), # Focal point is 1/3 across and 2/3 down in the crop box
)
def test_keeps_focal_point_in_view_bottom_left(self):
"""
Even though it tries to keep the composition of the image,
it shouldn't let that get in the way of keeping the entire subject in view
"""
self.assertEqual(
crop_to_point((300, 300), (150, 150), FocalPoint(x=100, y=200, width=150, height=150)),
CropBox(25, 125, 175, 275),
)
def test_keeps_focal_point_in_view_top_right(self):
"""
Even though it tries to keep the composition of the image,
it shouldn't let that get in the way of keeping the entire subject in view
"""
self.assertEqual(
crop_to_point((300, 300), (150, 150), FocalPoint(x=200, y=100, width=150, height=150)),
CropBox(125, 25, 275, 175),
)
class TestIssue573(TestCase):
"""
This tests for a bug which causes filename limit on Renditions to be reached
@ -1132,3 +1067,28 @@ class TestIssue613(TestCase, WagtailTestUtils):
# Check
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, image.id)
class TestIssue312(TestCase):
def test_duplicate_renditions(self):
# Create an image
image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
# Get two renditions and check that they're the same
rend1 = image.get_rendition('fill-100x100')
rend2 = image.get_rendition('fill-100x100')
self.assertEqual(rend1, rend2)
# Now manually duplicate the renditon and check that the database blocks it
self.assertRaises(
IntegrityError,
Rendition.objects.create,
image=rend1.image,
filter=rend1.filter,
width=rend1.width,
height=rend1.height,
focal_point_key=rend1.focal_point_key,
)

View file

@ -1,121 +0,0 @@
from __future__ import division
from wagtail.wagtailimages.utils.focal_point import FocalPoint
class CropBox(object):
def __init__(self, left, top, right, bottom):
self.left = int(left)
self.top = int(top)
self.right = int(right)
self.bottom = int(bottom)
def __getitem__(self, key):
return (self.left, self.top, self.right, self.bottom)[key]
@property
def width(self):
return self.right - self.left
@property
def height(self):
return self.bottom - self.top
@property
def size(self):
return self.width, self.height
def as_tuple(self):
return self.left, self.top, self.right, self.bottom
def __eq__(self, other):
return self.as_tuple() == other.as_tuple()
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return 'CropBox(left: %d, top: %d, right: %d, bottom: %d)' % (
self.left, self.top, self.right, self.bottom
)
def crop_to_centre(image_size, crop_size):
(original_width, original_height) = image_size
(crop_width, crop_height) = crop_size
# final dimensions should not exceed original dimensions
final_width = min(original_width, crop_width)
final_height = min(original_height, crop_height)
left = (original_width - final_width) / 2
top = (original_height - final_height) / 2
return CropBox(left, top, left + final_width, top + final_height)
def crop_to_point(image_size, crop_size, focal_point):
(original_width, original_height) = image_size
(crop_width, crop_height) = crop_size
if not focal_point:
focal_point = FocalPoint(original_width / 2, original_height / 2)
# Make sure that the crop size is no smaller than the focal point
crop_width = max(crop_width, focal_point.width)
crop_height = max(crop_height, focal_point.height)
# Make sure final dimensions do not exceed original dimensions
final_width = min(original_width, crop_width)
final_height = min(original_height, crop_height)
# Get UV for focal point
focal_point_u = focal_point.x / original_width
focal_point_v = focal_point.y / original_height
# Get crop box
left = focal_point.x - focal_point_u * final_width
top = focal_point.y - focal_point_v * final_height
right = focal_point.x - focal_point_u * final_width + final_width
bottom = focal_point.y - focal_point_v * final_height + final_height
# Make sure the entire focal point is in the crop box
focal_point_left = focal_point.x - focal_point.width / 2
focal_point_top = focal_point.y - focal_point.height / 2
focal_point_right = focal_point.x + focal_point.width / 2
focal_point_bottom = focal_point.y + focal_point.height / 2
if left > focal_point_left:
right -= left - focal_point_left
left = focal_point_left
if top > focal_point_top:
bottom -= top - focal_point_top
top = focal_point_top
if right < focal_point_right:
left += focal_point_right - right;
right = focal_point_right
if bottom < focal_point_bottom:
top += focal_point_bottom - bottom;
bottom = focal_point_bottom
# Don't allow the crop box to go over the image boundary
if left < 0:
right -= left
left = 0
if top < 0:
bottom -= top
top = 0
if right > original_width:
left -= right - original_width
right = original_width
if bottom > original_height:
top -= bottom - original_height
bottom = original_height
return CropBox(left, top, right, bottom)

View file

@ -0,0 +1,38 @@
from __future__ import division
class Rect(object):
def __init__(self, left, top, right, bottom):
self.left = int(left)
self.top = int(top)
self.right = int(right)
self.bottom = int(bottom)
def __getitem__(self, key):
return (self.left, self.top, self.right, self.bottom)[key]
@property
def width(self):
return self.right - self.left
@property
def height(self):
return self.bottom - self.top
@property
def size(self):
return self.width, self.height
def as_tuple(self):
return self.left, self.top, self.right, self.bottom
def __eq__(self, other):
return self.as_tuple() == other.as_tuple()
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return 'Rect(left: %d, top: %d, right: %d, bottom: %d)' % (
self.left, self.top, self.right, self.bottom
)

View file

@ -4,6 +4,8 @@ from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
@ -87,3 +89,10 @@ def editor_js():
""",
urlresolvers.reverse('wagtailimages_chooser')
)
@hooks.register('register_permissions')
def register_permissions():
image_content_type = ContentType.objects.get(app_label='wagtailimages', model='image')
image_permissions = Permission.objects.filter(content_type = image_content_type)
return image_permissions

View file

@ -20,6 +20,6 @@ class RedirectsMenuItem(MenuItem):
# TEMPORARY: Only show if the user is a superuser
return request.user.is_superuser
@hooks.register('register_admin_menu_item')
@hooks.register('register_settings_menu_item')
def register_redirects_menu_item():
return RedirectsMenuItem(_('Redirects'), urlresolvers.reverse('wagtailredirects_index'), classnames='icon icon-redirect', order=800)

View file

@ -3,6 +3,7 @@ from __future__ import absolute_import
import json
from six.moves.urllib.parse import urlparse
from six import text_type
from django.db import models
from django.db.models.sql.where import SubqueryConstraint, WhereNode
@ -140,7 +141,7 @@ class ElasticSearchQuery(object):
# Give error if the field doesn't exist
if field is None:
raise FieldError('Cannot filter ElasticSearch results with field "' + field_name + '". Please add FilterField(\'' + field_name + '\') to ' + self.queryset.model.__name__ + '.search_fields.')
raise FieldError('Cannot filter ElasticSearch results with field "' + field_attname + '". Please add FilterField(\'' + field_attname + '\') to ' + self.queryset.model.__name__ + '.search_fields.')
# Get the name of the field in the index
field_index_name = field.get_index_name(self.queryset.model)
@ -206,11 +207,11 @@ class ElasticSearchQuery(object):
if lookup == 'in':
return {
'terms': {
field_index_name: value,
field_index_name: list(value),
}
}
raise FilterError('Could not apply filter on ElasticSearch results: "' + field_name + '__' + lookup + ' = ' + unicode(value) + '". Lookup "' + lookup + '"" not recognosed.')
raise FilterError('Could not apply filter on ElasticSearch results: "' + field_attname + '__' + lookup + ' = ' + text_type(value) + '". Lookup "' + lookup + '"" not recognised.')
def _get_filters_from_where(self, where_node):
# Check if this is a leaf node

View file

@ -22,6 +22,7 @@ class EditorsPickForm(forms.ModelForm):
class Meta:
model = models.EditorsPick
fields = ('query', 'page', 'description')
widgets = {
'description': forms.Textarea(attrs=dict(rows=3)),

View file

@ -1,12 +1,15 @@
from optparse import make_option
from django.core.management.base import BaseCommand
from django.db import models
from django.conf import settings
from wagtail.wagtailsearch.indexed import Indexed
from wagtail.wagtailsearch.backends import get_search_backend
class Command(BaseCommand):
def handle(self, **options):
def get_object_list(self):
# Print info
self.stdout.write("Getting object list")
@ -41,26 +44,57 @@ class Command(BaseCommand):
# Space free, take it
object_set[key] = obj
# Search backend
if 'backend' in options:
s = options['backend']
else:
s = get_search_backend()
return indexed_models, object_set.values()
def update_backend(self, backend_name, models, object_list):
# Print info
self.stdout.write("Updating backend: " + backend_name)
# Get backend
backend = get_search_backend(backend_name)
# Reset the index
self.stdout.write("Reseting index")
s.reset_index()
self.stdout.write(backend_name + ": Reseting index")
backend.reset_index()
# Add types
self.stdout.write("Adding types")
for model in indexed_models:
s.add_type(model)
self.stdout.write(backend_name + ": Adding types")
for model in models:
backend.add_type(model)
# Add objects to index
self.stdout.write("Adding objects")
for result in s.add_bulk(object_set.values()):
self.stdout.write(backend_name + ": Adding objects")
for result in backend.add_bulk(object_list):
self.stdout.write(result[0] + ' ' + str(result[1]))
# Refresh index
self.stdout.write("Refreshing index")
s.refresh_index()
self.stdout.write(backend_name + ": Refreshing index")
backend.refresh_index()
option_list = BaseCommand.option_list + (
make_option('--backend',
action='store',
dest='backend_name',
default=None,
help="Specify a backend to update",
),
)
def handle(self, **options):
# Get object list
models, object_list = self.get_object_list()
# Get list of backends to index
if options['backend_name']:
# index only the passed backend
backend_names = [options['backend_name']]
elif hasattr(settings, 'WAGTAILSEARCH_BACKENDS'):
# index all backends listed in settings
backend_names = settings.WAGTAILSEARCH_BACKENDS.keys()
else:
# index the 'default' backend only
backend_names = ['default']
# Update backends
for backend_name in backend_names:
self.update_backend(backend_name, models, object_list)

View file

@ -8,9 +8,10 @@
<div class="nice-padding">
<div class="help-block help-info">
{% blocktrans %}
<p>Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like "<em>giving</em>".</p>
{% endblocktrans %}
{% blocktrans %}
<p>Promoted search results are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with the less common term "<em>giving</em>".</p>
{% endblocktrans %}
{% blocktrans %}
<p>The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, <em>including</em> any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.</p>
{% endblocktrans %}
</div>

View file

@ -6,7 +6,7 @@
{% include "wagtailadmin/shared/header.html" with title=delete_str subtitle=query.query_string %}
<div class="nice-padding">
<p>{% trans "Are you sure you want to delete all editors picks for this search term?" %}</p>
<p>{% trans "Are you sure you want to delete all promoted results for this search term?" %}</p>
<form action="{% url 'wagtailsearch_editorspicks_delete' query.id %}" method="POST">
{% csrf_token %}
<input type="submit" value="{% trans 'Yes, delete' %}" class="serious" />

View file

@ -7,13 +7,15 @@
</ul>
<fieldset>
<legend>{% trans "Editors pick" %}</legend>
<legend>{% trans "Promoted search result" %}</legend>
<ul class="fields">
<li class="model_choice_field">
{% trans "Choose another page" as choose_another_text_str %}
{% trans "Choose a page" as choose_one_text_str %}
{% if form.instance.page %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page page=form.instance.page is_chosen=True only %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page page=form.instance.page is_chosen=True choose_one_text_str=choose_one_text_str choose_another_text_str=choose_another_text_str only %}
{% else %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page is_chosen=False only %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.page is_chosen=False choose_one_text_str=choose_one_text_str choose_another_text_str=choose_another_text_str only %}
{% endif %}
</li>
<li class="char_field">

View file

@ -14,8 +14,8 @@
{% endblock %}
{% block content %}
{% trans "Editor's search picks" as sp_title_str %}
{% trans "Add new editor's pick" as sp_text_str %}
{% trans "Promoted search results" as sp_title_str %}
{% trans "Add new promoted result" as sp_text_str %}
{% include "wagtailadmin/shared/header.html" with title=sp_title_str add_link="wagtailsearch_editorspicks_add" icon="pick" add_text=sp_text_str search_url="wagtailsearch_editorspicks_index" %}
<div class="nice-padding">

View file

@ -6,7 +6,7 @@
<thead>
<tr>
<th class="title">{% trans "Search term(s)" %}</th>
<th>{% trans "Editors picks" %}</th>
<th>{% trans "Promoted results" %}</th>
<th>{% trans "Views (past week)" %}</th>
</tr>
</thead>

View file

@ -15,9 +15,9 @@
{% include "wagtailadmin/shared/pagination_nav.html" with items=queries is_searching=is_searching linkurl="wagtailsearch_editorspicks_index" %}
{% else %}
{% if is_searching %}
<p>{% blocktrans %}Sorry, no editor's picks match "<em>{{ query_string }}</em>"{% endblocktrans %}</p>
<p>{% blocktrans %}Sorry, no promoted results match "<em>{{ query_string }}</em>"{% endblocktrans %}</p>
{% else %}
{% url 'wagtailsearch_editorspicks_add' as wagtailsearch_editorspicks_add_url %}
<p>{% blocktrans %}No editor's picks have been created. Why not <a href="{{ wagtailsearch_editorspicks_add_url }}">add one</a>?{% endblocktrans %}</p>
<p>{% blocktrans %}No promoted results have been created. Why not <a href="{{ wagtailsearch_editorspicks_add_url }}">add one</a>?{% endblocktrans %}</p>
{% endif %}
{% endif %}

View file

@ -20,6 +20,7 @@ class BackendTests(WagtailTestUtils):
for backend_name, backend_conf in settings.WAGTAILSEARCH_BACKENDS.items():
if backend_conf['BACKEND'] == self.backend_path:
self.backend = get_search_backend(backend_name)
self.backend_name = backend_name
break
else:
# no conf entry found - skip tests for this backend
@ -103,6 +104,13 @@ class BackendTests(WagtailTestUtils):
# Should return two results
self.assertEqual(len(results), 2)
def test_filters_with_in_lookup(self):
live_page_titles = models.SearchTest.objects.filter(live=True).values_list('title', flat=True)
results = self.backend.search("Hello", models.SearchTest, filters=dict(title__in=live_page_titles))
# Should return two results
self.assertEqual(len(results), 2)
def test_single_result(self):
# Get a single result
result = self.backend.search("Hello", models.SearchTest)[0]
@ -146,7 +154,7 @@ class BackendTests(WagtailTestUtils):
# Run update_index command
with self.ignore_deprecation_warnings(): # ignore any DeprecationWarnings thrown by models with old-style indexed_fields definitions
management.call_command('update_index', backend=self.backend, interactive=False, stdout=StringIO())
management.call_command('update_index', backend_name=self.backend_name, interactive=False, stdout=StringIO())
# Check that there are still 3 results
results = self.backend.search("Hello", models.SearchTest)

View file

@ -24,6 +24,19 @@ class TestElasticSearchBackend(BackendTests, TestCase):
# Didn't crash, yay!
def test_filter_on_non_filterindex_field(self):
# id is not listed in the search_fields for SearchTest; this should raise a FieldError
from wagtail.wagtailsearch.backends.elasticsearch import FieldError
with self.assertRaises(FieldError):
results = list(self.backend.search("Hello", models.SearchTest, filters=dict(id=42)))
def test_filter_with_unsupported_lookup_type(self):
from wagtail.wagtailsearch.backends.elasticsearch import FilterError
with self.assertRaises(FilterError):
results = list(self.backend.search("Hello", models.SearchTest, filters=dict(title__iregex='h(ea)llo')))
def test_partial_search(self):
# Reset the index
self.backend.reset_index()

View file

@ -20,6 +20,6 @@ class EditorsPicksMenuItem(MenuItem):
# TEMPORARY: Only show if the user is a superuser
return request.user.is_superuser
@hooks.register('register_admin_menu_item')
@hooks.register('register_settings_menu_item')
def register_editors_picks_menu_item():
return EditorsPicksMenuItem(_('Editors picks'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900)
return EditorsPicksMenuItem(_('Promoted search results'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900)

View file

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