mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-05 14:04:46 +00:00
Merge remote-tracking branch 'torchbox/master' into cloudflare-cache-invalidation
This commit is contained in:
commit
51a30bfb54
140 changed files with 4485 additions and 1243 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Core components
|
|||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
sites
|
||||
pages/index
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------------------------------------
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
========================
|
||||
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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
|
||||
===================
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ INSTALLED_APPS = (
|
|||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsearch',
|
||||
|
|
|
|||
10
wagtail/tests/fixtures/test.json
vendored
10
wagtail/tests/fixtures/test.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
20
wagtail/tests/migrations/0004_auto_20141008_0420.py
Normal file
20
wagtail/tests/migrations/0004_auto_20141008_0420.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
|||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsearch',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
18
wagtail/wagtailadmin/static/wagtailadmin/js/submenu.js
Normal file
18
wagtail/wagtailadmin/static/wagtailadmin/js/submenu.js
Normal 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')
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -13,7 +13,7 @@
|
|||
<glyph unicode="1" 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="2" 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="3" 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="4" 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="4" 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="5" d="M135 424h241v-23h-241zM405 247l-127-124v222h-45v-220l-125 122-33-32 181-181 181 181z" />
|
||||
<glyph unicode="6" d="M136 424h241v-23h-241zM108 122l126 124v-222h45v220l126-122 32 32-181 181-181-181z" />
|
||||
<glyph unicode="7" 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="X" 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="Y" 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="Z" 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="^" 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="a" 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="b" 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="c" 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 |
Binary file not shown.
Binary file not shown.
|
|
@ -62,4 +62,4 @@ $color-text-input: $color-grey-1;
|
|||
|
||||
/* misc sizing */
|
||||
$thumbnail-width: 130px;
|
||||
$menu-width: 150px;
|
||||
$menu-width: 180px;
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')]),
|
||||
),
|
||||
]
|
||||
20
wagtail/wagtailcore/migrations/0004_page_locked.py
Normal file
20
wagtail/wagtailcore/migrations/0004_page_locked.py
Normal 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,
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
]
|
||||
19
wagtail/wagtailcore/migrations/0006_auto_20141008_0420.py
Normal file
19
wagtail/wagtailcore/migrations/0006_auto_20141008_0420.py
Normal 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')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class DocumentForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = ('title', 'file', 'tags')
|
||||
widgets = {
|
||||
'file': forms.FileInput()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
38
wagtail/wagtailimages/utils/rect.py
Normal file
38
wagtail/wagtailimages/utils/rect.py
Normal 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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
0
wagtail/wagtailsites/__init__.py
Normal file
0
wagtail/wagtailsites/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue