Merge branch 'master' into feature/crop-closeness

Conflicts:
	docs/core_components/pages/writing_templates.rst
This commit is contained in:
Karl Hobley 2014-10-04 13:50:45 +01:00
commit 4d954df20f
135 changed files with 3744 additions and 1117 deletions

View file

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

View file

@ -4,6 +4,21 @@ 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
* 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.
* 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: It is now easier to move pages to the beginning and end of their section
* Fix: Image rendering no longer creates erroneous duplicate Rendition records when the focal point is blank.
0.6 (11.09.2014)
~~~~~~~~~~~~~~~~

View file

@ -1,14 +1,14 @@
Original Authors
Authors
================
* Matthew Westcott matthew.westcott@torchbox.com twitter: @gasmanic
* David Cranwell david.cranwell@torchbox.com twitter: @davecranwell
* Karl Hobley karl.hobley@torchbox.com
* Helen Chapman helen.chapman@torchbox.com
Contributors
============
* Helen Chapman helen.chapman@torchbox.com
* Balazs Endresz balazs.endresz@torchbox.com
* Neal Todd neal.todd@torchbox.com
* Paul Hallett (twilio) hello@phalt.co
@ -46,6 +46,7 @@ Translators
* Greek: Serafeim Papastefanos
* Mongolian: Delgermurun Purevkhuu
* Polish: Łukasz Bołdys
* Portuguese: Jose Lourenco
* Portuguese Brazil: Gilson Filho
* Romanian: Dan Braghis
* Russian: ice9, HNKNTA

View file

@ -32,6 +32,13 @@ Find out more at `wagtail.io <http://wagtail.io/>`_.
Got a question? Ask it on our `Google Group <https://groups.google.com/forum/#!forum/wagtail>`_.
Who's using it?
~~~~~~~~~~~~~~~
We've a list of public Wagtail sites here: https://github.com/torchbox/wagtail/wiki/Public-Wagtail-sites
Got one of your own? Feel free to add it!
Getting started
~~~~~~~~~~~~~~~
* To get you up and running quickly, we've provided a demonstration site with all the configuration in place, at `github.com/torchbox/wagtaildemo <https://github.com/torchbox/wagtaildemo/>`_; see the `README <https://github.com/torchbox/wagtaildemo/blob/master/README.md>`_ for installation instructions.

View file

@ -1,6 +1,188 @@
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 images focal point if it has been set. If not, it will crop to the centre of the image.
**Cropping closer to the focal point**
By default, Wagtail will only crop to change the aspect ratio of the image.
In some cases (thumbnails, for example) it may be nice to crop closer to the focal point so the subject of the image is easier to see.
You can do this by appending ``-c<percentage>`` at the end of the method. For example, if you would like the image to be cropped as closely as possible to its focal point, add ``-c100`` to the end of the method.
.. code-block:: django
{% image self.photo fill-200x200-c100 %}
This will crop the image as much as it an but will never crop into the focal point.
If you find that ``-c100`` is too close, you can try ``-c75`` or ``-c50`` (any whole number from 0 to 100 is accepted).
``original``
(takes no dimensions)
.. code-block:: django
{% image self.photo original %}
Leaves the image at its original size - no resizing is performed.
.. Note::
Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
.. _image_tag_alt:
More control over the ``img`` tag
---------------------------------
Wagtail provides two shortcuts to give greater control over the ``img`` element:
**1. Adding attributes to the {% image %} tag**
.. versionadded:: 0.4
Extra attributes can be specified with the syntax ``attribute="value"``:
.. code-block:: django
{% image self.photo width-400 class="foo" id="bar" %}
No validation is performed on attributes added in this way so it's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
**2. Generating the image "as foo" to access individual properties**
Wagtail can assign the image data to another variable using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
This syntax exposes the underlying image "Rendition" (``tmp_photo``) to the developer. A "Rendition" contains just the information specific to the way you've requested to format the image i.e dimensions and source URL.
If your site defines a custom image model using ``AbstractImage``, then any additional fields you add to an image e.g a copyright holder, are **not** part of the image *rendition*, they're part of the image *model*.
Therefore in the above example, if you'd added the field ``foo`` to your AbstractImage you'd access it using ``{{ self.photo.foo }}`` not ``{{ tmp_photo.foo }}``.
(Due to the links in the database between renditions and their parent image, you could also access it as ``{{ tmp_photo.image.foo }}`` but this is clearly confusing.)
.. Note::
The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``.
The ``attrs`` shortcut
-----------------------
.. versionadded:: 0.4
You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
.. code-block:: django
<img {{ tmp_photo.attrs }} class="my-custom-class" />
Advanced topics
===============
.. toctree::
:maxdepth: 2

View file

@ -170,7 +170,7 @@ Reference
.. automethod:: search
See: :ref:`wagtailsearch_for_python_developers`
See: :ref:`wagtailsearch_searching_pages`
Example:
@ -178,3 +178,12 @@ 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)

View file

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

View file

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

View file

@ -52,7 +52,7 @@ A Parent node could provide its own function returning its descendant objects.
return events
This example makes sure to limit the returned objects to pieces of content which make sense, specifically ones which have been published through Wagtail's admin interface (``live()``) and are children of this node (``descendant_of(self)``). By setting a ``subpage_types`` class property in your model, you can specify which models are allowed to be set as children, but Wagtail will allow any ``Page``-derived model by default. Regardless, it's smart for a parent model to provide an index filtered to make sense.
This example makes sure to limit the returned objects to pieces of content which make sense, specifically ones which have been published through Wagtail's admin interface (``live()``) and are children of this node (``descendant_of(self)``). By setting a ``subpage_types`` class property in your model, you can specify which models are allowed to be set as children, and by setting a ``parent_page_types`` class property, you can specify which models are allowed to be parents of this page model. Wagtail will allow any ``Page``-derived model by default. Regardless, it's smart for a parent model to provide an index filtered to make sense.
Leaves
@ -71,7 +71,7 @@ The model for the leaf could provide a function that traverses the tree in the o
# Find closest ancestor which is an event index
return self.get_ancestors().type(EventIndexPage).last()
If defined, ``subpage_types`` will also limit the parent models allowed to contain a leaf. If not, Wagtail will allow any combination of parents and leafs to be associated in the Wagtail tree. Like with index pages, it's a good idea to make sure that the index is actually of the expected model to contain the leaf.
If defined, ``subpage_types`` and ``parent_page_types`` will also limit the parent models allowed to contain a leaf. If not, Wagtail will allow any combination of parents and leafs to be associated in the Wagtail tree. Like with index pages, it's a good idea to make sure that the index is actually of the expected model to contain the leaf.
Other Relationships

View file

@ -86,13 +86,9 @@ Template tags & filters
In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be ``load``-ed `as you would any other <https://docs.djangoproject.com/en/dev/topics/templates/#custom-tag-and-filter-libraries>`_
.. _image_tag:
Images (tag)
~~~~~~~~~~~~
.. versionchanged:: 0.4
The 'image_tags' tags library was renamed to 'wagtailimages_tags'
The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
@ -112,156 +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 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 images 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 shorcuts to give greater control over the ``img`` element:
**1. Adding attributes to the {% image %} tag**
.. versionadded:: 0.4
Extra attributes can be specified with the syntax ``attribute="value"``:
.. code-block:: django
{% image self.photo width-400 class="foo" id="bar" %}
No validation is performed on attributes added in this way so it's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
**2. Generating the image "as foo" to access individual properties**
Wagtail can assign the image data to another variable using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
This syntax exposes the underlying image "Rendition" (``tmp_photo``) to the developer. A "Rendition" contains just the information specific to the way you've requested to format the image i.e dimensions and source URL.
If your site defines a custom image model using ``AbstractImage``, then any additional fields you add to an image e.g a copyright holder, are **not** part of the image *rendition*, they're part of the image *model*.
Therefore in the above example, if you'd added the field ``foo`` to your AbstractImage you'd access it using ``{{ self.photo.foo }}`` not ``{{ tmp_photo.foo }}``.
(Due to the links in the database between renditions and their parent image, you could also access it as ``{{ tmp_photo.image.foo }}`` but this is clearly confusing.)
.. Note::
The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``.
The ``attrs`` shortcut
-----------------------
.. versionadded:: 0.4
You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
.. code-block:: django
<img {{ tmp_photo.attrs }} class="my-custom-class" />
See :ref:`image_tag` for full documentation.
.. _rich-text-filter:

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
Snippets
========
Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are models which do not inherit the ``Page`` class and are thus not organized into the Wagtail tree, but can still be made editable by assigning panels and identifying the model as a snippet with ``register_snippet()``.
Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are models which do not inherit the ``Page`` class and are thus not organized into the Wagtail tree, but can still be made editable by assigning panels and identifying the model as a snippet with the ``register_snippet`` class decorator.
Snippets are not search-able or order-able in the Wagtail admin, so decide carefully if the content type you would want to build into a snippet might be more suited to a page.
@ -19,9 +19,10 @@ Here's an example snippet from the Wagtail demo website:
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailsnippets.models import register_snippet
...
@register_snippet
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
@ -34,11 +35,9 @@ Here's an example snippet from the Wagtail demo website:
def __unicode__(self):
return self.text
register_snippet(Advert)
The ``Advert`` model uses the basic Django model class and defines two properties: text and url. The editing interface is very close to that provided for ``Page``-derived models, with fields assigned in the panels property. Snippets do not use multiple tabs of fields, nor do they provide the "save as draft" or "submit for moderation" features.
``register_snippet(Advert)`` tells Wagtail to treat the model as a snippet. The ``panels`` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through ``def __unicode__(self):`` so that the snippet objects make sense when listed in the Wagtail admin.
``@register_snippet`` tells Wagtail to treat the model as a snippet. The ``panels`` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through ``def __unicode__(self):`` so that the snippet objects make sense when listed in the Wagtail admin.
Including Snippets in Template Tags
-----------------------------------

View file

@ -1,3 +1,5 @@
.. _editor_manual:
Using Wagtail: an Editor's guide
================================

View file

@ -10,8 +10,10 @@ A basic Wagtail setup can be installed on your machine with only a few prerequis
Whether you just want to try out the demo site, or you're ready to dive in and create a Wagtail site with all bells and whistles enabled, we strongly recommend the Vagrant approach. Nevertheless, if you're the sort of person who balks at the idea of downloading a whole operating system just to run a web app, we've got you covered too. Start from `A basic Wagtail installation`_ below.
The no-installation route
=========================
The demo site (a.k.a. the no-installation route)
================================================
We provide a demo site containing a set of standard templates and page types - if you're new to Wagtail, this is the best way to try it out and familiarise yourself with how Wagtail works from the point of view of an editor.
If you're happy to use Vagrant, and you just want to set up the Wagtail demo site, or any other pre-existing Wagtail site that ships with Vagrant support, you don't need to install Wagtail at all. Install `Vagrant <http://www.vagrantup.com/>`__ and `VirtualBox <https://www.virtualbox.org/>`__, and run::
@ -27,7 +29,7 @@ Then, within the SSH session::
./manage.py runserver 0.0.0.0:8000
This will make the demo site available on your host machine at the URL http://localhost:8111/ - you can access the Wagtail admin interface at http://localhost:8111/admin/ .
This will make the demo site available on your host machine at the URL http://localhost:8111/ - you can access the Wagtail admin interface at http://localhost:8111/admin/ . Further instructions can be found at :ref:`editor_manual`.
Once youve experimented with the demo site and are ready to build your own site, it's time to install Wagtail on your host machine. Even if you intend to do all further Wagtail work within Vagrant, installing the Wagtail package on your host machine will provide the ``wagtail start`` command that sets up the initial file structure for your project.
@ -35,6 +37,8 @@ Once youve experimented with the demo site and are ready to build your own si
A basic Wagtail installation
============================
This provides everything you need to create a new Wagtail project from scratch, containing no page definitions or templates other than a basic homepage as a starting point for building your site. (For a gentler introduction to Wagtail, you may wish to try out the demo site first!)
You will need Python's `pip <http://pip.readthedocs.org/en/latest/installing.html>`__ package manager. We also recommend `virtualenvwrapper <http://virtualenvwrapper.readthedocs.org/en/latest/>`_ so that you can manage multiple independent Python environments for different projects - although this is not strictly necessary if you intend to do all your development under Vagrant.
Wagtail is based on the Django web framework and various other Python libraries. Most of these are pure Python and will install automatically using ``pip``, but there are a few native-code components that require further attention:
@ -59,9 +63,9 @@ You will now be able to run the following command to set up an initial file stru
**Without Vagrant:** Run the following steps to complete setup of your project (the ``migrate`` step will prompt you to set up a superuser account)::
cd myprojectname
./manage.py syncdb
./manage.py migrate
./manage.py runserver
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
Your site is now accessible at http://localhost:8000, with the admin backend available at http://localhost:8000/admin/ .

View file

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

View file

@ -52,6 +52,12 @@ All features deprecated in 0.4 have been removed
See: :ref:`04_deprecated_features`
Search signal handlers have been moved
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you have an import in your ``urls.py`` file like ``from wagtail.wagtailsearch import register_signal_handlers``, this must now be changed to ``from wagtail.wagtailsearch.signal_handlers import register_signal_handlers``
Deprecated features
===================

View file

@ -10,21 +10,57 @@ 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.
Minor features
~~~~~~~~~~~~~~
* The ``content_type`` template filter has been removed 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.
* 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.
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``).
* 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`.
Deprecated features
===================

View file

@ -5,6 +5,7 @@ Release notes
:maxdepth: 1
roadmap
0.7
0.6
0.5
0.4.1

View file

@ -3,3 +3,4 @@ coverage==3.7.1
flake8==2.2.1
mock==1.0.1
python-dateutil==2.2
pytz==2014.7

View file

@ -33,7 +33,7 @@ install_requires = [
"django-compressor>=1.4",
"django-libsass>=0.2",
"django-modelcluster>=0.4",
"django-taggit==0.12.1",
"django-taggit==0.12.2",
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
@ -62,7 +62,7 @@ setup(
license='BSD',
long_description=open('README.rst').read(),
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',

View file

@ -15,6 +15,7 @@ base =
elasticsearch==1.1.0
mock==1.0.1
python-dateutil==2.2
pytz==2014.7
Embedly
coverage

View file

@ -2,11 +2,11 @@
from __future__ import print_function, absolute_import
import os
import subprocess
import errno
import sys
from optparse import OptionParser
from django.core.management import ManagementUtility
def create_project(parser, options, args):
@ -44,15 +44,15 @@ def create_project(parser, options, args):
template_path = os.path.join(wagtail_path, 'project_template')
# Call django-admin startproject
result = subprocess.call([
utility = ManagementUtility([
'django-admin.py', 'startproject',
'--template=' + template_path,
'--name=Vagrantfile', '--ext=html,rst',
project_name
])
utility.execute()
if result == 0:
print("Success! %(project_name)s is created" % {'project_name': project_name})
print("Success! %(project_name)s is created" % {'project_name': project_name})
COMMANDS = {

View file

@ -33,6 +33,7 @@
<li><a href="#progress">Progress indicators</a></li>
<li><a href="#misc">Misc formatters</a></li>
<li><a href="#icons">Icons</a></li>
<li><a href="#ie9">IE9 debugging</a></li>
</ul>
</nav>
@ -514,6 +515,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 +531,30 @@
{% block extra_js %}
<script>
$(function(){
// Debugging for stylesheet problems
var styleSheets = document.styleSheets, totalStyleSheets = styleSheets.length;
for (var j = 0; j < totalStyleSheets; j++) {
var styleSheet = styleSheets[j], rules = styleSheet.cssRules, totalSelectorsInStylesheet = 0, style = "";
var totalRulesInStylesheet = rules ? rules.length : 0;
for (var i = 0; i < totalRulesInStylesheet; i++) {
if (rules[i].selectorText) {
try {
totalSelectorsInStylesheet += rules[i].selectorText.split(',').length;
}
catch (err) {
console.log(err);
}
}
}
if(totalSelectorsInStylesheet > 4095){
style = 'color:red';
}
$('#ie9-debug').append("<h3>" + styleSheet.href + "</h3>" + "<p>Total rules: <strong>" + totalRulesInStylesheet + "</strong>. " + "Total selectors: <strong style='" + style + "'>" + totalSelectorsInStylesheet + "</strong></p>");
}
(function runprogress(){
var to = setTimeout(function(){
runprogress();

View file

@ -1,87 +1,24 @@
# -*- 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
from django.db import models, migrations
class Migration(SchemaMigration):
class Migration(migrations.Migration):
def forwards(self, orm):
# Adding model 'HomePage'
db.create_table('core_homepage', (
('page_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['wagtailcore.Page'], unique=True, primary_key=True)),
))
db.send_create_signal('core', ['HomePage'])
dependencies = [
('wagtailcore', '0002_initial_data'),
]
def backwards(self, orm):
# Deleting model 'HomePage'
db.delete_table('core_homepage')
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'})
},
'core.homepage': {
'Meta': {'object_name': 'HomePage', '_ormbases': ['wagtailcore.Page']},
'page_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['wagtailcore.Page']", 'unique': 'True', 'primary_key': 'True'})
},
'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'})
}
}
complete_apps = ['core']
operations = [
migrations.CreateModel(
name='HomePage',
fields=[
('page_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
]

View file

@ -1,114 +1,45 @@
# -*- 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
from django.db import models, migrations
class Migration(DataMigration):
def create_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
Page = apps.get_model('wagtailcore.Page')
Site = apps.get_model('wagtailcore.Site')
HomePage = apps.get_model('core.HomePage')
depends_on = (
('wagtailcore', '0002_initial_data'),
# Delete the default homepage
Page.objects.get(id=2).delete()
# Create content type for homepage model
homepage_content_type, created = ContentType.objects.get_or_create(
model='homepage', app_label='core', defaults={'name': 'Homepage'})
# Create a new homepage
homepage = HomePage.objects.create(
title="Homepage",
slug='home',
content_type=homepage_content_type,
path='00010001',
depth=2,
numchild=0,
url_path='/home/',
)
def forwards(self, orm):
if connection.vendor == 'sqlite':
set_autocommit(True)
# Create a site with the new homepage set as the root
Site.objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
orm['wagtailcore.Page'].objects.get(id=2).delete()
homepage_content_type, created = orm['contenttypes.contenttype'].objects.get_or_create(
model='homepage', app_label='core', defaults={'name': 'Homepage'})
class Migration(migrations.Migration):
homepage = orm['core.HomePage'].objects.create(
title="Homepage",
slug='home',
content_type=homepage_content_type,
path='00010001',
depth=2,
numchild=0,
url_path='/home/',
)
dependencies = [
('core', '0001_initial'),
]
orm['wagtailcore.site'].objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
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'})
},
'core.homepage': {
'Meta': {'object_name': 'HomePage', '_ormbases': ['wagtailcore.Page']},
'page_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['wagtailcore.Page']", 'unique': 'True', 'primary_key': 'True'})
},
'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.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 = ['core']
symmetrical = True
operations = [
migrations.RunPython(create_homepage),
]

View file

@ -3,7 +3,6 @@
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

View file

@ -5,7 +5,6 @@
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

View file

@ -2,7 +2,7 @@
{% templatetag openblock %} load static core_tags {% templatetag closeblock %}
{% templatetag openblock %} block body_class {% templatetag closeblock %}template-{% templatetag openvariable %} self|content_type|slugify {% templatetag closevariable %}{% templatetag openblock %} endblock {% templatetag closeblock %}
{% templatetag openblock %} block body_class {% templatetag closeblock %}template-{% templatetag openvariable %} self.get_verbose_name|slugify {% templatetag closevariable %}{% templatetag openblock %} endblock {% templatetag closeblock %}
{% templatetag openblock %} block content {% templatetag closeblock %}
<h1>Welcome to your new Wagtail site!</h1>

View file

@ -1,12 +1,3 @@
from django import template
from django.conf import settings
register = template.Library()
# Return the model name/"content type" as a string e.g BlogPage, NewsListingPage.
# Can be used with "slugify" to create CSS-friendly classnames
# Usage: {% verbatim %}{{ self|content_type|slugify }}{% endverbatim %}
@register.filter
def content_type(model):
return model.__class__.__name__

View file

@ -38,7 +38,6 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'compressor',
'taggit',
'modelcluster',
@ -48,6 +47,7 @@ INSTALLED_APPS = (
'wagtail.wagtaildocs',
'wagtail.wagtailsnippets',
'wagtail.wagtailusers',
'wagtail.wagtailsites',
'wagtail.wagtailimages',
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',

View file

@ -1,6 +1,5 @@
# Minimal requirements
Django>=1.6.2,<1.7
South==1.0.0
Django>=1.7,<1.8
wagtail==0.6
# Recommended components (require additional setup):

View file

@ -22,8 +22,7 @@ chmod a+x $PROJECT_DIR/manage.py
# Run syncdb/migrate/update_index
su - vagrant -c "$PYTHON $PROJECT_DIR/manage.py syncdb --noinput && \
$PYTHON $PROJECT_DIR/manage.py migrate --noinput && \
su - vagrant -c "$PYTHON $PROJECT_DIR/manage.py migrate --noinput && \
$PYTHON $PROJECT_DIR/manage.py update_index"

View file

@ -376,7 +376,9 @@ class ZuluSnippet(models.Model):
class StandardIndex(Page):
pass
""" Index for the site, not allowed to be placed anywhere """
parent_page_types = []
StandardIndex.content_panels = [
FieldPanel('title', classname="full title"),
@ -387,14 +389,22 @@ StandardIndex.content_panels = [
class StandardChild(Page):
pass
class BusinessIndex(Page):
""" Can be placed anywhere, can only have Business children """
subpage_types = ['tests.BusinessChild', 'tests.BusinessSubIndex']
class BusinessSubIndex(Page):
""" Can be placed under BusinessIndex, and have BusinessChild children """
subpage_types = ['tests.BusinessChild']
parent_page_types = ['tests.BusinessIndex']
class BusinessChild(Page):
""" Can only be placed under Business indexes, no children allowed """
subpage_types = []
parent_page_types = ['tests.BusinessIndex', BusinessSubIndex]
class SearchTest(models.Model, index.Indexed):

View file

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

View file

@ -19,8 +19,8 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.utils import camelcase_to_underscore
from wagtail.wagtailcore.fields import RichTextArea
from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_string
FORM_FIELD_OVERRIDES = {}
@ -483,31 +483,33 @@ class BasePageChooserPanel(BaseChooserPanel):
def target_content_type(cls):
if cls._target_content_type is None:
if cls.page_type:
if isinstance(cls.page_type, string_types):
# translate the passed model name into an actual model class
from django.db.models import get_model
try:
app_label, model_name = cls.page_type.split('.')
except ValueError:
raise ImproperlyConfigured("The page_type passed to PageChooserPanel must be of the form 'app_label.model_name'")
try:
model = resolve_model_string(cls.page_type)
except LookupError:
raise ImproperlyConfigured("{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
cls.__name__, cls.page_type))
except ValueError:
raise ImproperlyConfigured("{0}.page_type refers to model {1!r} that has not been installed".format(
cls.__name__, cls.page_type))
try:
page_type = get_model(app_label, model_name)
except LookupError:
page_type = None
if page_type is None:
raise ImproperlyConfigured("PageChooserPanel refers to model '%s' that has not been installed" % cls.page_type)
else:
page_type = cls.page_type
cls._target_content_type = ContentType.objects.get_for_model(page_type)
cls._target_content_type = ContentType.objects.get_for_model(model)
else:
# TODO: infer the content type by introspection on the foreign key
cls._target_content_type = ContentType.objects.get_by_natural_key('wagtailcore', 'page')
return cls._target_content_type
def render_as_field(self, show_help_text=True):
instance_obj = self.get_chosen_item()
return mark_safe(render_to_string(self.field_template, {
'field': self.bound_field,
self.object_type_name: instance_obj,
'is_chosen': bool(instance_obj),
'show_help_text': show_help_text,
'choose_another_text_str': ugettext_lazy("Choose another page"),
'choose_one_text_str': ugettext_lazy("Choose a page"),
}))
def render_js(self):
page = self.get_chosen_item()
parent = page.get_parent() if page else None

View file

@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: Wagtail 0.5.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-09-11 16:37+0100\n"
"PO-Revision-Date: 2014-08-03 01:50+0100\n"
"PO-Revision-Date: 2014-09-17 17:21+0100\n"
"Last-Translator: Jose Lourenco <jose@lourenco.ws>\n"
"Language-Team: \n"
"Language: pt_PT\n"
@ -293,6 +293,10 @@ msgid ""
"Search results will exclude pages of other types.\n"
" "
msgstr ""
"\n"
" Neste campo apenas pode escolher páginas do tipo \"%(type)s\". "
"Nos resultados da pesquisa serão excluídas as páginas de outros tipos.\n"
" "
#: templates/wagtailadmin/chooser/_search_results.html:16
#, python-format
@ -314,9 +318,8 @@ msgstr[1] ""
" "
#: templates/wagtailadmin/chooser/browse.html:3
#, fuzzy
msgid "Choose"
msgstr "Escolher uma página"
msgstr "Escolhe"
#: templates/wagtailadmin/chooser/browse.html:6
#: templates/wagtailadmin/chooser/search.html:2
@ -581,11 +584,10 @@ msgid "You can edit the privacy settings on:"
msgstr "Você pode editar as configurações de privacidade em:"
#: templates/wagtailadmin/page_privacy/set_privacy.html:6
#, fuzzy
msgid "Privacy changes apply to all children of this page too."
msgstr ""
"<b>Nota:</b> as alterações de privacidade também serão aplicadas a todas as "
"páginas filhas desta página."
"As alterações de privacidade também serão aplicadas a todas as páginas "
"filhas desta página."
#: templates/wagtailadmin/pages/_moderator_userbar.html:4
#, python-format

View file

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

View file

@ -122,7 +122,7 @@ function InlinePanel(opts) {
var self = {};
self.setHasContent = function(){
if($('li:visible', self.formsUl).length){
if($('> li', self.formsUl).not(".deleted").length){
self.formsUl.parent().removeClass('empty');
}else{
self.formsUl.parent().addClass('empty');
@ -139,7 +139,7 @@ function InlinePanel(opts) {
$('#' + deleteInputId + '-button').click(function() {
/* set 'deleted' form field to true */
$('#' + deleteInputId).val('1');
$('#' + childId).slideUp(function() {
$('#' + childId).addClass('deleted').slideUp(function() {
self.updateMoveButtonDisabledStates();
self.setHasContent();
});
@ -191,8 +191,8 @@ function InlinePanel(opts) {
/* Hide container on page load if it is marked as deleted. Remove the error
message so that it doesn't count towards the number of errors on the tab at the
top of the page. */
if ( $('#' + deleteInputId).val() === "1" ) {
$('#' + childId).hide(0, function() {
if ($('#' + deleteInputId).val() === "1" ) {
$('#' + childId).addClass('deleted').hide(0, function() {
self.updateMoveButtonDisabledStates();
self.setHasContent();
});

View file

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

View file

@ -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{

View file

@ -0,0 +1,383 @@
$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;
}
ul{
border-top:1px solid rgba(100,100,100,0.2);
}
li{
@include transition(border-color 0.2s ease);
border-bottom:1px solid rgba(100,100,100,0.2);
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;
label{
@include visuallyhidden();
}
input, button{
font-size:1em;
border:0;
@include border-radius(0);
}
input{
background-color:darken($color-grey-2, 15%);
color:#AAA;
padding: 0.9em 2.5em 0.9em 2em;
&: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: 164px; /* 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:164px; /* same as .nav-main minus 1 pixel for border */
}
.dl-menu {
position: absolute;
}
body.nav-open{
.content-wrapper{
position:relative;
}
}
body.explorer-open {
.wrapper{
-webkit-transform:none;
transform:none;
}
.nav-wrapper{
margin-left: 0;
width: $menu-width;
}
.explorer:before{
display:none;
}
.nav-main{
display:block;
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
/*! jQuery UI - v1.10.3 - 2013-09-26
/*! jQuery UI - v1.10.4 - 2014-09-12
* http://jqueryui.com
* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Open Sans%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.4em&cornerRadius=0&bgColorHeader=%23246060&bgTextureHeader=flat&bgImgOpacityHeader=100&borderColorHeader=%23246060&fcHeader=%23ffffff&iconColorHeader=%23ffffff&bgColorContent=%23ffffff&bgTextureContent=flat&bgImgOpacityContent=100&borderColorContent=%23d8d8d8&fcContent=%23222222&iconColorContent=%23222222&bgColorDefault=%23ffffff&bgTextureDefault=flat&bgImgOpacityDefault=100&borderColorDefault=%23d3d3d3&fcDefault=%23555555&iconColorDefault=%23ffffff&bgColorHover=%2349c0c1&bgTextureHover=flat&bgImgOpacityHover=100&borderColorHover=%2349c0c1&fcHover=%23ffffff&iconColorHover=%23ffffff&bgColorActive=%23ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=%23aaaaaa&fcActive=%23212121&iconColorActive=%23454545&bgColorHighlight=%23e8f8f9&bgTextureHighlight=flat&bgImgOpacityHighlight=100&borderColorHighlight=%23e8f8f9&fcHighlight=%23363636&iconColorHighlight=%2349c0c1&bgColorError=%23f7474e&bgTextureError=flat&bgImgOpacityError=100&borderColorError=%23f7474e&fcError=%23ffffff&iconColorError=%23ffffff&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=%23aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright 2013 jQuery Foundation and other contributors; Licensed MIT */
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Open%20Sans%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.2em&cornerRadius=0&bgColorHeader=%23246060&bgTextureHeader=flat&bgImgOpacityHeader=100&borderColorHeader=%23246060&fcHeader=%23ffffff&iconColorHeader=%23ffffff&bgColorContent=%23ffffff&bgTextureContent=flat&bgImgOpacityContent=100&borderColorContent=%23d8d8d8&fcContent=%23222222&iconColorContent=%23222222&bgColorDefault=%23ffffff&bgTextureDefault=flat&bgImgOpacityDefault=100&borderColorDefault=%23d3d3d3&fcDefault=%23555555&iconColorDefault=%23555555&bgColorHover=%2349c0c1&bgTextureHover=flat&bgImgOpacityHover=100&borderColorHover=%2349c0c1&fcHover=%23ffffff&iconColorHover=%23ffffff&bgColorActive=%2349c0c1&bgTextureActive=flat&bgImgOpacityActive=65&borderColorActive=%23aaaaaa&fcActive=%23ffffff&iconColorActive=%23ffffff&bgColorHighlight=%23e8f8f9&bgTextureHighlight=flat&bgImgOpacityHighlight=100&borderColorHighlight=%23e8f8f9&fcHighlight=%23363636&iconColorHighlight=%2349c0c1&bgColorError=%23f7474e&bgTextureError=flat&bgImgOpacityError=100&borderColorError=%23f7474e&fcError=%23ffffff&iconColorError=%23ffffff&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=%23aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
/* Layout helpers
----------------------------------*/
@ -347,9 +347,6 @@ button.ui-button::-moz-focus-inner {
font-size: 1em;
margin: 1px 0;
}
.ui-datepicker select.ui-datepicker-month-year {
width: 100%;
}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year {
width: 49%;
@ -466,6 +463,7 @@ button.ui-button::-moz-focus-inner {
border-left-width: 1px;
}
.ui-dialog {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
@ -488,7 +486,7 @@ button.ui-button::-moz-focus-inner {
position: absolute;
right: .3em;
top: 50%;
width: 21px;
width: 20px;
margin: -10px 0 0 0;
padding: 1px;
height: 20px;
@ -704,13 +702,13 @@ button.ui-button::-moz-focus-inner {
overflow: hidden;
right: 0;
}
/* more specificity required here to overide default borders */
/* more specificity required here to override default borders */
.ui-spinner a.ui-spinner-button {
border-top: none;
border-bottom: none;
border-right: none;
}
/* vertical centre icon */
/* vertically center icon */
.ui-spinner .ui-icon {
position: absolute;
margin-top: -8px;
@ -747,7 +745,7 @@ button.ui-button::-moz-focus-inner {
padding: 0;
white-space: nowrap;
}
.ui-tabs .ui-tabs-nav li a {
.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
float: left;
padding: .5em 1em;
text-decoration: none;
@ -756,13 +754,12 @@ button.ui-button::-moz-focus-inner {
margin-bottom: -1px;
padding-bottom: 1px;
}
.ui-tabs .ui-tabs-nav li.ui-tabs-active a,
.ui-tabs .ui-tabs-nav li.ui-state-disabled a,
.ui-tabs .ui-tabs-nav li.ui-tabs-loading a {
.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
cursor: text;
}
.ui-tabs .ui-tabs-nav li a, /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a {
.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
cursor: pointer;
}
.ui-tabs .ui-tabs-panel {
@ -801,7 +798,7 @@ body .ui-tooltip {
}
.ui-widget-content {
border: 1px solid #d8d8d8;
background: #ffffff url(images/ui-bg_flat_100_ffffff_40x100.png) 50% 50% repeat-x;
background: #ffffff url("images/ui-bg_flat_100_ffffff_40x100.png") 50% 50% repeat-x;
color: #222222;
}
.ui-widget-content a {
@ -809,7 +806,7 @@ body .ui-tooltip {
}
.ui-widget-header {
border: 1px solid #246060;
background: #246060 url(images/ui-bg_flat_100_246060_40x100.png) 50% 50% repeat-x;
background: #246060 url("images/ui-bg_flat_100_246060_40x100.png") 50% 50% repeat-x;
color: #ffffff;
font-weight: bold;
}
@ -823,7 +820,7 @@ body .ui-tooltip {
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
border: 1px solid #d3d3d3;
background: #ffffff url(images/ui-bg_flat_100_ffffff_40x100.png) 50% 50% repeat-x;
background: #ffffff url("images/ui-bg_flat_100_ffffff_40x100.png") 50% 50% repeat-x;
font-weight: normal;
color: #555555;
}
@ -840,14 +837,18 @@ body .ui-tooltip {
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus {
border: 1px solid #49c0c1;
background: #49c0c1 url(images/ui-bg_flat_100_49c0c1_40x100.png) 50% 50% repeat-x;
background: #49c0c1 url("images/ui-bg_flat_100_49c0c1_40x100.png") 50% 50% repeat-x;
font-weight: normal;
color: #ffffff;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited {
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited {
color: #ffffff;
text-decoration: none;
}
@ -855,14 +856,14 @@ body .ui-tooltip {
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
border: 1px solid #aaaaaa;
background: #49c0c1 url(images/ui-bg_glass_65_49c0c1_1x400.png) 50% 50% repeat-x !important;
background: #49c0c1 url("images/ui-bg_flat_65_49c0c1_40x100.png") 50% 50% repeat-x;
font-weight: normal;
color: white;
color: #ffffff;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #212121;
color: #ffffff;
text-decoration: none;
}
@ -872,7 +873,7 @@ body .ui-tooltip {
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #e8f8f9;
background: #e8f8f9 url(images/ui-bg_flat_100_e8f8f9_40x100.png) 50% 50% repeat-x;
background: #e8f8f9 url("images/ui-bg_flat_100_e8f8f9_40x100.png") 50% 50% repeat-x;
color: #363636;
}
.ui-state-highlight a,
@ -884,7 +885,7 @@ body .ui-tooltip {
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #f7474e;
background: #f7474e url(images/ui-bg_flat_100_f7474e_40x100.png) 50% 50% repeat-x;
background: #f7474e url("images/ui-bg_flat_100_f7474e_40x100.png") 50% 50% repeat-x;
color: #ffffff;
}
.ui-state-error a,
@ -930,27 +931,27 @@ body .ui-tooltip {
}
.ui-icon,
.ui-widget-content .ui-icon {
background-image: url(images/ui-icons_222222_256x240.png);
background-image: url("images/ui-icons_222222_256x240.png");
}
.ui-widget-header .ui-icon {
background-image: url(images/ui-icons_ffffff_256x240.png);
background-image: url("images/ui-icons_ffffff_256x240.png");
}
.ui-state-default .ui-icon {
background-image: url(images/ui-icons_ffffff_256x240.png);
background-image: url("images/ui-icons_555555_256x240.png");
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon {
background-image: url(images/ui-icons_ffffff_256x240.png);
background-image: url("images/ui-icons_ffffff_256x240.png");
}
.ui-state-active .ui-icon {
background-image: url(images/ui-icons_454545_256x240.png);
background-image: url("images/ui-icons_ffffff_256x240.png");
}
.ui-state-highlight .ui-icon {
background-image: url(images/ui-icons_49c0c1_256x240.png);
background-image: url("images/ui-icons_49c0c1_256x240.png");
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url(images/ui-icons_ffffff_256x240.png);
background-image: url("images/ui-icons_ffffff_256x240.png");
}
/* positioning */
@ -1140,37 +1141,37 @@ body .ui-tooltip {
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
border-top-left-radius: 3px;
border-top-left-radius: 0;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
border-top-right-radius: 3px;
border-top-right-radius: 0;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
border-bottom-left-radius: 3px;
border-bottom-left-radius: 0;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
border-bottom-right-radius: 3px;
border-bottom-right-radius: 0;
}
/* Overlays */
.ui-widget-overlay {
background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
background: #aaaaaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;
opacity: .3;
filter: Alpha(Opacity=30);
}
.ui-widget-shadow {
margin: -8px 0 0 -8px;
padding: 8px;
background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
background: #aaaaaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;
opacity: .3;
filter: Alpha(Opacity=30);
border-radius: 8px;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,11 @@ from django.core import mail
from django.core.paginator import Paginator
from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, EventPageCarouselItem, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex, TaggedPage, Advert, AdvertPlacement
from wagtail.tests.models import (
SimplePage, EventPage, EventPageCarouselItem,
StandardIndex, StandardChild,
BusinessIndex, BusinessChild, BusinessSubIndex,
TaggedPage, Advert, AdvertPlacement)
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published, page_unpublished
@ -1483,11 +1487,14 @@ class TestSubpageBusinessRules(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertContains(response, add_subpage_url)
# add_subpage should give us the full set of page types to choose
# add_subpage should give us choices of StandardChild, and BusinessIndex.
# BusinessSubIndex and BusinessChild are not allowed
response = self.client.get(add_subpage_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Standard Child')
self.assertContains(response, 'Business Child')
self.assertContains(response, StandardChild.get_verbose_name())
self.assertContains(response, BusinessIndex.get_verbose_name())
self.assertNotContains(response, BusinessSubIndex.get_verbose_name())
self.assertNotContains(response, BusinessChild.get_verbose_name())
def test_business_subpage(self):
add_subpage_url = reverse('wagtailadmin_pages_add_subpage', args=(self.business_index.id, ))
@ -1500,8 +1507,10 @@ class TestSubpageBusinessRules(TestCase, WagtailTestUtils):
# add_subpage should give us a cut-down set of page types to choose
response = self.client.get(add_subpage_url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'Standard Child')
self.assertContains(response, 'Business Child')
self.assertNotContains(response, StandardIndex.get_verbose_name())
self.assertNotContains(response, StandardChild.get_verbose_name())
self.assertContains(response, BusinessSubIndex.get_verbose_name())
self.assertContains(response, BusinessChild.get_verbose_name())
def test_business_child_subpage(self):
add_subpage_url = reverse('wagtailadmin_pages_add_subpage', args=(self.business_child.id, ))
@ -1516,12 +1525,16 @@ class TestSubpageBusinessRules(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 403)
def test_cannot_add_invalid_subpage_type(self):
# cannot add SimplePage as a child of BusinessIndex, as SimplePage is not present in subpage_types
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.business_index.id)))
# cannot add StandardChild as a child of BusinessIndex, as StandardChild is not present in subpage_types
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'standardchild', self.business_index.id)))
self.assertEqual(response.status_code, 403)
# likewise for BusinessChild which has an empty subpage_types list
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.business_child.id)))
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'standardchild', self.business_child.id)))
self.assertEqual(response.status_code, 403)
# cannot add BusinessChild to StandardIndex, as BusinessChild restricts is parent page types
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'businesschild', self.standard_index.id)))
self.assertEqual(response.status_code, 403)
# but we can add a BusinessChild to BusinessIndex

View file

@ -68,7 +68,7 @@ def add_subpage(request, parent_page_id):
if not parent_page.permissions_for_user(request.user).can_add_subpage():
raise PermissionDenied
page_types = sorted(parent_page.clean_subpage_types(), key=lambda pagetype: pagetype.name.lower())
page_types = sorted(parent_page.allowed_subpage_types(), key=lambda pagetype: pagetype.name.lower())
if len(page_types) == 1:
# Only one page type is available - redirect straight to the create form rather than
@ -136,7 +136,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
raise Http404
# page must be in the list of allowed subpage types for this parent ID
if content_type not in parent_page.clean_subpage_types():
if content_type not in parent_page.allowed_subpage_types():
raise PermissionDenied
page = page_class(owner=request.user)
@ -634,8 +634,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')

View file

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

View file

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

View file

@ -8,7 +8,7 @@ from six.moves.urllib.parse import urlparse
from modelcluster.models import ClusterableModel, get_all_child_relations
from django.db import models, connection, transaction
from django.db.models import get_model, Q
from django.db.models import Q
from django.http import Http404
from django.core.cache import cache
from django.core.handlers.wsgi import WSGIRequest
@ -20,13 +20,13 @@ from django.conf import settings
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
from treebeard.mp_tree import MP_Node
from wagtail.wagtailcore.utils import camelcase_to_underscore
from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_string
from wagtail.wagtailcore.query import PageQuerySet
from wagtail.wagtailcore.url_routing import RouteResult
@ -58,25 +58,25 @@ class Site(models.Model):
@staticmethod
def find_for_request(request):
"""
Find the site object responsible for responding to this HTTP
request object. Try:
- unique hostname first
- then hostname and port
- if there is no matching hostname at all, or no matching
hostname:port combination, fall back to the unique default site,
or raise an exception
NB this means that high-numbered ports on an extant hostname may
still be routed to a different hostname which is set as the default
Find the site object responsible for responding to this HTTP
request object. Try:
- unique hostname first
- then hostname and port
- if there is no matching hostname at all, or no matching
hostname:port combination, fall back to the unique default site,
or raise an exception
NB this means that high-numbered ports on an extant hostname may
still be routed to a different hostname which is set as the default
"""
try:
hostname = request.META['HTTP_HOST'].split(':')[0] # 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 +106,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):
@ -236,6 +236,7 @@ class PageBase(models.base.ModelBase):
cls.ajax_template = None
cls._clean_subpage_types = None # to be filled in on first call to cls.clean_subpage_types
cls._clean_parent_page_types = None # to be filled in on first call to cls.clean_parent_page_types
if not dct.get('is_abstract'):
# subclasses are only abstract if the subclass itself defines itself so
@ -267,7 +268,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
expired = 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'),
@ -354,8 +355,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
@ -504,8 +504,8 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
@classmethod
def clean_subpage_types(cls):
"""
Returns the list of subpage types, with strings converted to class objects
where required
Returns the list of subpage types, with strings converted to class objects
where required
"""
if cls._clean_subpage_types is None:
subpage_types = getattr(cls, 'subpage_types', None)
@ -513,43 +513,64 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
# if subpage_types is not specified on the Page class, allow all page types as subpages
res = get_page_types()
else:
res = []
for page_type in cls.subpage_types:
if isinstance(page_type, string_types):
try:
app_label, model_name = page_type.split(".")
except ValueError:
# If we can't split, assume a model in current app
app_label = cls._meta.app_label
model_name = page_type
model = get_model(app_label, model_name)
if model:
res.append(ContentType.objects.get_for_model(model))
else:
raise NameError(_("name '{0}' (used in subpage_types list) is not defined.").format(page_type))
else:
# assume it's already a model class
res.append(ContentType.objects.get_for_model(page_type))
try:
models = [resolve_model_string(model_string, cls._meta.app_label)
for model_string in subpage_types]
except LookupError as err:
raise ImproperlyConfigured("{0}.subpage_types must be a list of 'app_label.model_name' strings, given {1!r}".format(
cls.__name__, err.args[1]))
res = list(map(ContentType.objects.get_for_model, models))
cls._clean_subpage_types = res
return cls._clean_subpage_types
@classmethod
def allowed_parent_page_types(cls):
def clean_parent_page_types(cls):
"""
Returns the list of page types that this page type can be a subpage of
Returns the list of parent page types, with strings converted to class
objects where required
"""
return [ct for ct in get_page_types() if cls in ct.model_class().clean_subpage_types()]
if cls._clean_parent_page_types is None:
parent_page_types = getattr(cls, 'parent_page_types', None)
if parent_page_types is None:
# if parent_page_types is not specified on the Page class, allow all page types as subpages
res = get_page_types()
else:
try:
models = [resolve_model_string(model_string, cls._meta.app_label)
for model_string in parent_page_types]
except LookupError as err:
raise ImproperlyConfigured("{0}.parent_page_types must be a list of 'app_label.model_name' strings, given {1!r}".format(
cls.__name__, err.args[1]))
res = list(map(ContentType.objects.get_for_model, models))
cls._clean_parent_page_types = res
return cls._clean_parent_page_types
@classmethod
def allowed_parent_pages(cls):
def allowed_parent_page_types(cls):
"""
Returns the list of pages that this page type can be a subpage of
Returns the list of page types that this page type can be a subpage of
"""
return Page.objects.filter(content_type__in=cls.allowed_parent_page_types())
cls_ct = ContentType.objects.get_for_model(cls)
return [ct for ct in cls.clean_parent_page_types()
if cls_ct in ct.model_class().clean_subpage_types()]
@classmethod
def allowed_subpage_types(cls):
"""
Returns the list of page types that this page type can be a subpage of
"""
# Special case the 'Page' class, such as the Root page or Home page -
# otherwise you can not add initial pages when setting up a site
if cls == Page:
return get_page_types()
cls_ct = ContentType.objects.get_for_model(cls)
return [ct for ct in cls.clean_subpage_types()
if cls_ct in ct.model_class().clean_parent_page_types()]
@classmethod
def get_verbose_name(cls):
@ -601,7 +622,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
new_self.save()
new_self._update_descendant_url_paths(old_url_path, new_url_path)
def copy(self, recursive=False, to=None, update_attrs=None):
def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True):
# Make a copy
page_copy = Page.objects.get(id=self.id).specific
page_copy.pk = None
@ -631,6 +652,15 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
setattr(child_object, parental_key_name, page_copy.id)
child_object.save()
# Copy revisions
if copy_revisions:
for revision in self.revisions.all():
revision.pk = None
revision.submitted_for_moderation = False
revision.approved_go_live_at = None
revision.page = page_copy
revision.save()
# Copy child pages
if recursive:
for child_page in self.get_children():
@ -795,7 +825,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)
@ -910,9 +940,9 @@ class PageRevision(models.Model):
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'),
]
@ -921,6 +951,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
@ -1012,7 +1045,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(
@ -1023,7 +1056,7 @@ class PagePermissionTester(object):
def can_add_subpage(self):
if not self.user.is_active:
return False
if not self.page.specific_class.clean_subpage_types(): # this page model has an empty subpage_types list, so no subpages are allowed
if not self.page.specific_class.allowed_subpage_types(): # this page model has an empty subpage_types list, so no subpages are allowed
return False
return self.user.is_superuser or ('add' in self.permissions)
@ -1087,7 +1120,7 @@ class PagePermissionTester(object):
"""
if not self.user.is_active:
return False
if not self.page.specific_class.clean_subpage_types(): # this page model has an empty subpage_types list, so no subpages are allowed
if not self.page.specific_class.allowed_subpage_types(): # this page model has an empty subpage_types list, so no subpages are allowed
return False
return self.user.is_superuser or ('publish' in self.permissions)

View file

@ -1,4 +1,4 @@
from django.db.models import Q
from django.db.models import Q, get_models
from django.contrib.contenttypes.models import ContentType
from treebeard.mp_tree import MP_NodeQuerySet
@ -152,13 +152,17 @@ class PageQuerySet(MP_NodeQuerySet):
"""
return self.exclude(self.sibling_of_q(other, inclusive))
def type_q(self, model):
content_type = ContentType.objects.get_for_model(model)
return Q(content_type=content_type)
def type_q(self, klass):
content_types = ContentType.objects.get_for_models(*[
model for model in get_models()
if issubclass(model, klass)
]).values()
return Q(content_type__in=content_types)
def type(self, model):
"""
This filters the queryset to only contain pages that are an instance of the specified model.
This filters the queryset to only contain pages that are an instance of the specified model (including subclasses).
"""
return self.filter(self.type_q(model))

View file

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

View file

@ -1,11 +1,15 @@
import warnings
import datetime
import pytz
from django.test import TestCase, Client
from django.test.utils import override_settings
from django.http import HttpRequest, Http404
from django.contrib.contenttypes.models import ContentType
from wagtail.wagtailcore.models import Page, Site
from wagtail.tests.models import EventPage, EventIndex, SimplePage, PageWithOldStyleRouteMethod
from wagtail.tests.models import EventPage, EventIndex, SimplePage, PageWithOldStyleRouteMethod, BusinessIndex, BusinessSubIndex, BusinessChild, StandardIndex
class TestSiteRouting(TestCase):
@ -413,6 +417,45 @@ class TestCopyPage(TestCase):
self.assertEqual(new_christmas_event.advert_placements.count(), 1, "Child objects defined on the superclass weren't copied")
self.assertEqual(christmas_event.advert_placements.count(), 1, "Child objects defined on the superclass were removed from the original page")
def test_copy_page_copies_revisions(self):
christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
christmas_event.save_revision()
# Copy it
new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
# Check that the revisions were copied
self.assertEqual(new_christmas_event.revisions.count(), 1, "Revisions weren't copied")
# Check that the revisions weren't removed from old page
self.assertEqual(christmas_event.revisions.count(), 1, "Revisions were removed from the original page")
def test_copy_page_copies_revisions_and_doesnt_submit_for_moderation(self):
christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
christmas_event.save_revision(submitted_for_moderation=True)
# Copy it
new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
# Check that the old revision is still submitted for moderation
self.assertTrue(christmas_event.revisions.first().submitted_for_moderation)
# Check that the new revision is not submitted for moderation
self.assertFalse(new_christmas_event.revisions.first().submitted_for_moderation)
def test_copy_page_copies_revisions_and_doesnt_schedule(self):
christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
christmas_event.save_revision(approved_go_live_at=datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=pytz.utc))
# Copy it
new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
# Check that the old revision is still scheduled
self.assertEqual(christmas_event.revisions.first().approved_go_live_at, datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=pytz.utc))
# Check that the new revision is not scheduled
self.assertEqual(new_christmas_event.revisions.first().approved_go_live_at, None)
def test_copy_page_copies_child_objects_with_nonspecific_class(self):
# Get chrismas page as Page instead of EventPage
christmas_event = Page.objects.get(url_path='/home/events/christmas/')
@ -458,3 +501,55 @@ class TestCopyPage(TestCase):
# Check that the speakers weren't removed from old page
self.assertEqual(old_christmas_event.specific.speakers.count(), 1, "Child objects were removed from the original page")
def test_copy_page_copies_recursively_with_revisions(self):
events_index = EventIndex.objects.get(url_path='/home/events/')
old_christmas_event = events_index.get_children().filter(slug='christmas').first()
old_christmas_event.save_revision()
# Copy it
new_events_index = events_index.copy(recursive=True, update_attrs={'title': "New events index", 'slug': 'new-events-index'})
# Get christmas event
new_christmas_event = new_events_index.get_children().filter(slug='christmas').first()
# Check that the revisions were copied
self.assertEqual(new_christmas_event.specific.revisions.count(), 1, "Revisions weren't copied")
# Check that the revisions weren't removed from old page
self.assertEqual(old_christmas_event.specific.revisions.count(), 1, "Revisions were removed from the original page")
class TestSubpageTypeBusinessRules(TestCase):
def test_allowed_subpage_types(self):
# SimplePage does not define any restrictions on subpage types
# SimplePage is a valid subpage of SimplePage
self.assertIn(ContentType.objects.get_for_model(SimplePage), SimplePage.allowed_subpage_types())
# BusinessIndex is a valid subpage of SimplePage
self.assertIn(ContentType.objects.get_for_model(BusinessIndex), SimplePage.allowed_subpage_types())
# BusinessSubIndex is not valid, because it explicitly omits SimplePage from parent_page_types
self.assertNotIn(ContentType.objects.get_for_model(BusinessSubIndex), SimplePage.allowed_subpage_types())
# BusinessChild has an empty subpage_types list, so does not allow anything
self.assertNotIn(ContentType.objects.get_for_model(SimplePage), BusinessChild.allowed_subpage_types())
self.assertNotIn(ContentType.objects.get_for_model(BusinessIndex), BusinessChild.allowed_subpage_types())
self.assertNotIn(ContentType.objects.get_for_model(BusinessSubIndex), BusinessChild.allowed_subpage_types())
# BusinessSubIndex only allows BusinessChild as subpage type
self.assertNotIn(ContentType.objects.get_for_model(SimplePage), BusinessSubIndex.allowed_subpage_types())
self.assertIn(ContentType.objects.get_for_model(BusinessChild), BusinessSubIndex.allowed_subpage_types())
def test_allowed_parent_page_types(self):
# SimplePage does not define any restrictions on parent page types
# SimplePage is a valid parent page of SimplePage
self.assertIn(ContentType.objects.get_for_model(SimplePage), SimplePage.allowed_parent_page_types())
# BusinessChild cannot be a parent of anything
self.assertNotIn(ContentType.objects.get_for_model(BusinessChild), SimplePage.allowed_parent_page_types())
# StandardIndex does not allow anything as a parent
self.assertNotIn(ContentType.objects.get_for_model(SimplePage), StandardIndex.allowed_parent_page_types())
self.assertNotIn(ContentType.objects.get_for_model(StandardIndex), StandardIndex.allowed_parent_page_types())
# BusinessSubIndex only allows BusinessIndex as a parent
self.assertNotIn(ContentType.objects.get_for_model(SimplePage), BusinessSubIndex.allowed_parent_page_types())
self.assertIn(ContentType.objects.get_for_model(BusinessIndex), BusinessSubIndex.allowed_parent_page_types())

View file

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

View file

@ -1,6 +1,38 @@
import re
from django.db.models import Model, get_model
from six import string_types
def camelcase_to_underscore(str):
# http://djangosnippets.org/snippets/585/
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', str).lower().strip('_')
def resolve_model_string(model_string, default_app=None):
"""
Resolve an 'app_label.model_name' string into an actual model class.
If a model class is passed in, just return that.
"""
if isinstance(model_string, string_types):
try:
app_label, model_name = model_string.split(".")
except ValueError:
if default_app is not None:
# If we can't split, assume a model in current app
app_label = default_app
model_name = model_string
else:
raise ValueError("Can not resolve {0!r} into a model. Model names "
"should be in the form app_label.model_name".format(
model_string), model_string)
model = get_model(app_label, model_name)
if not model:
raise LookupError("Can not resolve {0!r} into a model".format(model_string), model_string)
return model
elif isinstance(model_string, type) and issubclass(model_string, Model):
return model_string
else:
raise LookupError("Can not resolve {0!r} into a model".format(model_string), model_string)

View file

@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from django.test.utils import override_settings
from wagtail.tests.utils import WagtailTestUtils
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import EventPage, EventPageRelatedLink
@ -432,7 +432,7 @@ class TestIssue613(TestCase, WagtailTestUtils):
return get_search_backend(backend_name)
else:
# no conf entry found - skip tests for this backend
raise unittest.SkipTest("No WAGTAILSEARCH_BACKENDS entry for the backend %s" % self.backend_path)
raise unittest.SkipTest("No WAGTAILSEARCH_BACKENDS entry for the backend %s" % backend_path)
def setUp(self):
self.search_backend = self.get_elasticsearch_backend()

View file

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

View file

@ -1,4 +1 @@
from .models import Embed
from .embeds import get_embed
default_app_config = 'wagtail.wagtailembeds.apps.WagtailEmbedsAppConfig'

View file

@ -2,7 +2,7 @@ from __future__ import division # Use true division
from django.template.loader import render_to_string
from wagtail.wagtailembeds import get_embed
from wagtail.wagtailembeds.embeds import get_embed
def embed_to_frontend_html(url):

View file

@ -3,7 +3,7 @@ import warnings
from django import template
from django.utils.safestring import mark_safe
from wagtail.wagtailembeds import get_embed
from wagtail.wagtailembeds.embeds import get_embed
register = template.Library()

View file

@ -15,13 +15,14 @@ from django.test import TestCase
from wagtail.tests.utils import WagtailTestUtils, unittest
from wagtail.wagtailembeds import get_embed
from wagtail.wagtailembeds.embeds import (
EmbedNotFoundException,
EmbedlyException,
AccessDeniedEmbedlyException,
get_embed,
embedly as wagtail_embedly,
oembed as wagtail_oembed,
)
from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly, oembed as wagtail_oembed
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter

View file

@ -0,0 +1,136 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Wagtail\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-09-11 16:38+0100\n"
"PO-Revision-Date: 2014-09-17 17:32+0100\n"
"Last-Translator: Jose Lourenco <jose@lourenco.ws>\n"
"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/"
"wagtail/language/pt_PT/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt_PT\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 1.5.4\n"
#: models.py:22
msgid "Single line text"
msgstr "Linha de texto"
#: models.py:23
msgid "Multi-line text"
msgstr "Caixa de linhas de texto"
#: models.py:24
msgid "Email"
msgstr "Email"
#: models.py:25
msgid "Number"
msgstr "Número"
#: models.py:26
msgid "URL"
msgstr "URL"
#: models.py:27
msgid "Checkbox"
msgstr "Caixa de seleção"
#: models.py:28
msgid "Checkboxes"
msgstr "Caixas de seleção"
#: models.py:29
msgid "Drop down"
msgstr "Lista pendente"
#: models.py:30
msgid "Radio buttons"
msgstr "Botões de seleção"
#: models.py:31
msgid "Date"
msgstr "Data"
#: models.py:32
msgid "Date/time"
msgstr "Data/hora"
#: models.py:60
msgid "The label of the form field"
msgstr "O nome do campo do formulário"
#: models.py:67
msgid ""
"Comma seperated list of choices. Only applicable in checkboxes, radio and "
"dropdown."
msgstr ""
"Lista de opções separadas por vírgula. Só utilizável nas caixas de seleção, "
"botões e listas pendentes."
#: models.py:72
msgid "Default value. Comma seperated values supported for checkboxes."
msgstr ""
"Valor pré-definido. Valores separados por vírgula admitidos nas caixas de "
"seleção."
#: models.py:191
msgid "Optional - form submissions will be emailed to this address"
msgstr ""
"Opcional - as submissões de formulários serão enviadas por email para este "
"endereço"
#: wagtail_hooks.py:25 templates/wagtailforms/index.html:3
#: templates/wagtailforms/index.html:6
msgid "Forms"
msgstr "Formulários"
#: templates/wagtailforms/index.html:7
msgid "Pages"
msgstr "Páginas"
#: templates/wagtailforms/index_submissions.html:3
#, python-format
msgid "Submissions of %(form_title)s"
msgstr "Submissões de %(form_title)s"
#: templates/wagtailforms/index_submissions.html:36
#, python-format
msgid "Form data <span>%(form_title)s</span>"
msgstr "Dados do formulário <span>%(form_title)s</span>"
#: templates/wagtailforms/index_submissions.html:45
msgid "Filter"
msgstr "Filtrar"
#: templates/wagtailforms/index_submissions.html:51
msgid "Download CSV"
msgstr "Descarregar CSV"
#: templates/wagtailforms/index_submissions.html:63
#, python-format
msgid "There have been no submissions of the '%(title)s' form."
msgstr "Não houve submissões do formulário '%(title)s'."
#: templates/wagtailforms/list_forms.html:7
msgid "Title"
msgstr "Título"
#: templates/wagtailforms/list_forms.html:8
msgid "Origin"
msgstr "Origem"
#: templates/wagtailforms/list_submissions.html:9
msgid "Submission Date"
msgstr "Data de submissão"
#: templates/wagtailforms/results_forms.html:7
msgid "No form pages have been created."
msgstr "Não foram criadas páginas de formulários."

View file

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

View file

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

View file

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

View file

@ -143,7 +143,7 @@ class AbstractImage(models.Model, TagSearchable):
else:
rendition = self.renditions.get(
filter=filter,
focal_point_key=None,
focal_point_key='',
)
except ObjectDoesNotExist:
file_field = self.file
@ -175,7 +175,6 @@ class AbstractImage(models.Model, TagSearchable):
else:
rendition, created = self.renditions.get_or_create(
filter=filter,
focal_point_key=None,
defaults={'file': generated_image_file}
)
@ -353,7 +352,7 @@ class AbstractRendition(models.Model):
file = models.ImageField(upload_to='images', width_field='width', height_field='height')
width = models.IntegerField(editable=False)
height = models.IntegerField(editable=False)
focal_point_key = models.CharField(max_length=255, null=True, editable=False)
focal_point_key = models.CharField(max_length=255, blank=True, default='', editable=False)
@property
def url(self):

View file

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'Rendition.focal_point_key'
db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, default=''))
def backwards(self, orm):
# Changing field 'Rendition.focal_point_key'
db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'wagtailimages.filter': {
'Meta': {'object_name': 'Filter'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'spec': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
},
'wagtailimages.image': {
'Meta': {'object_name': 'Image'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'focal_point_height': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_width': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_x': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'focal_point_y': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'height': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'uploaded_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'width': ('django.db.models.fields.IntegerField', [], {})
},
'wagtailimages.rendition': {
'Meta': {'unique_together': "(('image', 'filter', 'focal_point_key'),)", 'object_name': 'Rendition'},
'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['wagtailimages.Filter']"}),
'focal_point_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'height': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'renditions'", 'to': "orm['wagtailimages.Image']"}),
'width': ('django.db.models.fields.IntegerField', [], {})
}
}
complete_apps = ['wagtailimages']

View file

@ -1,6 +1,6 @@
@import "../../wagtailadmin/static/wagtailadmin/scss/variables.scss";
@import "../../wagtailadmin/static/wagtailadmin/scss/mixins.scss";
@import "../../wagtailadmin/static/wagtailadmin/scss/grid.scss";
@import "wagtailadmin/scss/variables.scss";
@import "wagtailadmin/scss/mixins.scss";
@import "wagtailadmin/scss/grid.scss";
.replace-file-input{
display:inline-block;

View file

@ -1,5 +1,5 @@
@import "../../wagtailadmin/static/wagtailadmin/scss/variables.scss";
@import "../../wagtailadmin/static/wagtailadmin/scss/mixins.scss";
@import "wagtailadmin/scss/variables.scss";
@import "wagtailadmin/scss/mixins.scss";
.focal-point-chooser {
position: relative;

View file

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

View file

@ -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,
@ -781,9 +782,6 @@ class TestFrontendServeView(TestCase):
self.assertEqual(response['Cache-Control'].split('=')[0], 'max-age')
self.assertTrue(int(response['Cache-Control'].split('=')[1]) > datetime.timedelta(days=30).seconds)
self.assertIn('Expires', response)
self.assertTrue(dateutil.parser.parse(response['Expires']) > timezone.now() + datetime.timedelta(days=30))
def test_get_invalid_signature(self):
"""
Test that an invalid signature returns a 403 response
@ -985,7 +983,7 @@ class TestIssue613(TestCase, WagtailTestUtils):
return get_search_backend(backend_name)
else:
# no conf entry found - skip tests for this backend
raise unittest.SkipTest("No WAGTAILSEARCH_BACKENDS entry for the backend %s" % self.backend_path)
raise unittest.SkipTest("No WAGTAILSEARCH_BACKENDS entry for the backend %s" % backend_path)
def setUp(self):
self.search_backend = self.get_elasticsearch_backend()
@ -1069,3 +1067,28 @@ class TestIssue613(TestCase, WagtailTestUtils):
# Check
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, image.id)
class TestIssue312(TestCase):
def test_duplicate_renditions(self):
# Create an image
image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
# Get two renditions and check that they're the same
rend1 = image.get_rendition('fill-100x100')
rend2 = image.get_rendition('fill-100x100')
self.assertEqual(rend1, rend2)
# Now manually duplicate the renditon and check that the database blocks it
self.assertRaises(
IntegrityError,
Rendition.objects.create,
image=rend1.image,
filter=rend1.filter,
width=rend1.width,
height=rend1.height,
focal_point_key=rend1.focal_point_key,
)

View file

@ -1,13 +1,13 @@
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.core.exceptions import PermissionDenied
from django.views.decorators.cache import cache_page
from django.views.decorators.cache import cache_control
from wagtail.wagtailimages.models import get_image_model, Filter
from wagtail.wagtailimages.utils.crypto import verify_signature
@cache_page(60 * 60 * 24 * 60) # Cache for 60 days
@cache_control(max_age=60*60*24*60) # Cache for 60 days
def serve(request, signature, image_id, filter_spec):
image = get_object_or_404(get_image_model(), id=image_id)

View file

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

View file

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

View file

@ -1,5 +1 @@
from wagtail.wagtailsearch.index import Indexed
from wagtail.wagtailsearch.signal_handlers import register_signal_handlers
from wagtail.wagtailsearch.backends import get_search_backend
default_app_config = 'wagtail.wagtailsearch.apps.WagtailSearchAppConfig'

View file

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

View file

@ -1,7 +1,8 @@
from django.core.management.base import BaseCommand
from django.db import models
from wagtail.wagtailsearch import Indexed, get_search_backend
from wagtail.wagtailsearch.indexed import Indexed
from wagtail.wagtailsearch.backends import get_search_backend
class Command(BaseCommand):

View file

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

View file

@ -103,6 +103,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]

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from wagtail.tests.utils import unittest
import datetime
import json
@ -21,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()
@ -65,6 +81,28 @@ class TestElasticSearchBackend(BackendTests, TestCase):
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, obj.id)
def test_ascii_folding(self):
# Reset the index
self.backend.reset_index()
self.backend.add_type(models.SearchTest)
self.backend.add_type(models.SearchTestChild)
# Add some test data
obj = models.SearchTest()
obj.title = "Ĥéỻø"
obj.live = True
obj.save()
self.backend.add(obj)
# Refresh the index
self.backend.refresh_index()
# Search and check
results = self.backend.search("Hello", models.SearchTest.objects.all())
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, obj.id)
class TestElasticSearchQuery(TestCase):
def assertDictEqual(self, a, b):

View file

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

View file

View file

@ -0,0 +1,14 @@
from django import forms
from wagtail.wagtailcore.models import Site
class SiteForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SiteForm, self).__init__(*args, **kwargs)
self.fields['root_page'].widget = forms.HiddenInput()
required_css_class = "required"
class Meta:
model = Site

View file

@ -0,0 +1,109 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-03 16:23+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: views.py:31
msgid "Site '{0}' created."
msgstr ""
#: views.py:34
msgid "The site could not be created due to errors."
msgstr ""
#: views.py:51
msgid "Site '{0}' updated."
msgstr ""
#: views.py:54
msgid "The site could not be saved due to errors."
msgstr ""
#: views.py:70
msgid "Site '{0}' deleted."
msgstr ""
#: wagtail_hooks.py:24 templates/wagtailsites/index.html:3
#: templates/wagtailsites/index.html:5
msgid "Sites"
msgstr ""
#: templates/wagtailsites/confirm_delete.html:3
#: templates/wagtailsites/confirm_delete.html:7
#: templates/wagtailsites/edit.html:36
msgid "Delete site"
msgstr ""
#: templates/wagtailsites/confirm_delete.html:11
msgid "Are you sure you want to delete this site?"
msgstr ""
#: templates/wagtailsites/confirm_delete.html:14
msgid "Yes, delete"
msgstr ""
#: templates/wagtailsites/create.html:4 templates/wagtailsites/create.html:9
msgid "Add site"
msgstr ""
#: templates/wagtailsites/create.html:20
msgid "Choose a different root page"
msgstr ""
#: templates/wagtailsites/create.html:21
msgid "Choose a root page"
msgstr ""
#: templates/wagtailsites/create.html:31 templates/wagtailsites/edit.html:34
msgid "Save"
msgstr ""
#: templates/wagtailsites/edit.html:4 templates/wagtailsites/edit.html.py:9
msgid "Editing"
msgstr ""
#: templates/wagtailsites/edit.html:21
msgid "Change page"
msgstr ""
#: templates/wagtailsites/edit.html:22
msgid "Choose page"
msgstr ""
#: templates/wagtailsites/index.html:7
msgid "Add a site"
msgstr ""
#: templates/wagtailsites/index.html:19
msgid "Site"
msgstr ""
#: templates/wagtailsites/index.html:26
msgid "Port"
msgstr ""
#: templates/wagtailsites/index.html:27
msgid "Root page"
msgstr ""
#: templates/wagtailsites/index.html:28
msgid "Default?"
msgstr ""
#: templates/wagtailsites/index.html:41
msgid "Default"
msgstr ""

View file

@ -0,0 +1,17 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Delete site" %}{% endblock %}
{% block bodyclass %}menu-site{% endblock %}
{% block content %}
{% trans "Delete site" as del_str %}
{% include "wagtailadmin/shared/header.html" with title=del_str subtitle=site.hostname icon="site" %}
<div class="row row-flush nice-padding">
<p>{% trans "Are you sure you want to delete this site?" %}</p>
<form action="{% url 'wagtailsites_delete' site.id %}" method="POST">
{% csrf_token %}
<input type="submit" value="{% trans 'Yes, delete' %}" class="serious" />
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Add site" %} {{ site.hostname }}{% endblock %}
{% block bodyclass %}menu-sites{% endblock %}
{% block content %}
{% trans "Add site" as add_site_str %}
{% include "wagtailadmin/shared/header.html" with title=add_site_str icon="site" %}
<form action="{% url 'wagtailsites_create' %}" method="POST">
{% csrf_token %}
<div class="nice-padding">
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.hostname %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.port %}
<li>
{% trans "Choose a different root page" as choose_another_text_str %}
{% trans "Choose a root page" as choose_one_text_str %}
{% if form.instance.root_page %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.root_page page=form.instance.root_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.root_page is_chosen=False choose_one_text_str=choose_one_text_str choose_another_text_str=choose_another_text_str only %}
{% endif %}
</li>
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_default_site %}
<li><input type="submit" value="{% trans 'Save' %}" /></li>
</ul>
</div>
</form>
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
<script type="text/javascript">
{% include "wagtailsites/includes/site_form.js" %}
</script>
{% endblock %}

View file

@ -0,0 +1,50 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Editing" %} {{ site.hostname }}{% endblock %}
{% block bodyclass %}menu-sites{% endblock %}
{% block content %}
{% trans "Editing" as editing_str %}
{% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=site.hostname icon="site" %}
<div class="nice-padding">
<form action="{% url 'wagtailsites_edit' site.id %}" method="POST">
{% csrf_token %}
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.hostname %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.port %}
<li>
{% trans "Change page" as choose_another_text_str %}
{% trans "Choose page" as choose_one_text_str %}
{% if form.instance.root_page %}
{% include "wagtailadmin/edit_handlers/page_chooser_panel.html" with field=form.root_page page=form.instance.root_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.root_page is_chosen=False choose_one_text_str=choose_one_text_str choose_another_text_str=choose_another_text_str only %}
{% endif %}
</li>
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_default_site %}
<li>
<input type="submit" value="{% trans 'Save' %}" />
{% if perms.site.delete_site %}
<a href="{% url 'wagtailsites_delete' site.id %}" class="button button-secondary no">{% trans "Delete site" %}</a>
{% endif %}
</li>
</ul>
</form>
</div>
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
<script>
{% include "wagtailsites/includes/site_form.js" %}
</script>
{% endblock %}

View file

@ -0,0 +1,5 @@
(function() {
function fixPrefix(str) { return str; }
createPageChooser(fixPrefix('id_root_page'));
})();

View file

@ -0,0 +1,49 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Sites" %}{% endblock %}
{% block content %}
{% trans "Sites" as sites_str %}
{% if perms.site.add_site %}
{% trans "Add a site" as add_a_site_str %}
{% include "wagtailadmin/shared/header.html" with title=sites_str add_link="wagtailsites_create" add_text=add_a_site_str icon="site" %}
{% else %}
{% include "wagtailadmin/shared/header.html" with title=sites_str icon="site" %}
{% endif %}
<div class="nice-padding">
<div id="sites-list">
<table class="listing">
<thead>
<tr>
<th class="hostname">
{% trans "Site" %}
{% if ordering == "site" %}
<a href="{% url 'wagtailsites_index' %}" class="icon icon-arrow-down-after teal"></a>
{% else %}
<a href="{% url 'wagtailsites_index' %}?ordering=name" class="icon icon-arrow-down-after"></a>
{% endif %}
</th>
<th class="port">{% trans "Port" %}</th>
<th class="root-page">{% trans "Root page" %}</th>
<th class="is-default-site">{% trans "Default?" %}</th>
</tr>
</thead>
<tbody>
{% for site in sites %}
<tr>
<td class="hostname title">
<h2>
<a href="{% url 'wagtailsites_edit' site.id %}">{{ site.hostname }}</a>
</h2>
</td>
<td class="port">{{ site.port }}</td>
<td class="root-page">{{ site.root_page }}</td>
<td class="is-default-site"><div class="status-tag {% if site.is_default_site %}primary{% endif %}">{% if site.is_default_site %}{% trans "Default" %}{% endif %}</div></td>
</li>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

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