diff --git a/.tx/config b/.tx/config index b3b2e98be..4312d6d79 100644 --- a/.tx/config +++ b/.tx/config @@ -60,3 +60,9 @@ file_filter = wagtail/wagtailforms/locale//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//LC_MESSAGES/django.po +source_file = wagtail/wagtailsites/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd1de0e13..8a602fd30 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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) ~~~~~~~~~~~~~~~~ diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 67f6c853a..881453e64 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -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 diff --git a/README.rst b/README.rst index 3a74ec8cd..1ea2dcd3d 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,13 @@ Find out more at `wagtail.io `_. Got a question? Ask it on our `Google Group `_. +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 `_; see the `README `_ for installation instructions. diff --git a/docs/core_components/images/index.rst b/docs/core_components/images/index.rst index fcae2d837..45ac14833 100644 --- a/docs/core_components/images/index.rst +++ b/docs/core_components/images/index.rst @@ -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 %} + + + {% 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`` 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 %} + + {{ tmp_photo.alt }} + + +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 + + + + + +Advanced topics +=============== + .. toctree:: :maxdepth: 2 diff --git a/docs/core_components/pages/advanced_topics/queryset_methods.rst b/docs/core_components/pages/advanced_topics/queryset_methods.rst index 2c221b883..afbaf3c3c 100644 --- a/docs/core_components/pages/advanced_topics/queryset_methods.rst +++ b/docs/core_components/pages/advanced_topics/queryset_methods.rst @@ -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) diff --git a/docs/core_components/pages/creating_pages.rst b/docs/core_components/pages/creating_pages.rst index 52d187890..c31c0b113 100644 --- a/docs/core_components/pages/creating_pages.rst +++ b/docs/core_components/pages/creating_pages.rst @@ -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 diff --git a/docs/core_components/pages/editing_api.rst b/docs/core_components/pages/editing_api.rst index e94792b59..94bbb8e88 100644 --- a/docs/core_components/pages/editing_api.rst +++ b/docs/core_components/pages/editing_api.rst @@ -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 ------------------------------------- diff --git a/docs/core_components/pages/theory.rst b/docs/core_components/pages/theory.rst index bcea079bb..639ee34f2 100644 --- a/docs/core_components/pages/theory.rst +++ b/docs/core_components/pages/theory.rst @@ -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 diff --git a/docs/core_components/pages/writing_templates.rst b/docs/core_components/pages/writing_templates.rst index 37ea6663d..7a879c92e 100644 --- a/docs/core_components/pages/writing_templates.rst +++ b/docs/core_components/pages/writing_templates.rst @@ -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 `_ -.. _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: {% 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`` 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 %} - - {{ tmp_photo.alt }} - - -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 - - +See :ref:`image_tag` for full documentation. .. _rich-text-filter: diff --git a/docs/core_components/search/editors_picks.rst b/docs/core_components/search/editors_picks.rst deleted file mode 100644 index 59ae647de..000000000 --- a/docs/core_components/search/editors_picks.rst +++ /dev/null @@ -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 %} -
-

Editors picks

- -
- {% endif %} - {% endwith %} diff --git a/docs/core_components/search/index.rst b/docs/core_components/search/index.rst index e343f76c8..f8e889b5e 100644 --- a/docs/core_components/search/index.rst +++ b/docs/core_components/search/index.rst @@ -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` diff --git a/docs/core_components/search/for_python_developers.rst b/docs/core_components/search/indexing.rst similarity index 66% rename from docs/core_components/search/for_python_developers.rst rename to docs/core_components/search/indexing.rst index b254a2e1f..953bb85b3 100644 --- a/docs/core_components/search/for_python_developers.rst +++ b/docs/core_components/search/indexing.rst @@ -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") - [, ] +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 ======================== diff --git a/docs/core_components/search/frontend_views.rst b/docs/core_components/search/searching.rst similarity index 65% rename from docs/core_components/search/frontend_views.rst rename to docs/core_components/search/searching.rst index 1e1ad3fd3..a5876d77f 100644 --- a/docs/core_components/search/frontend_views.rst +++ b/docs/core_components/search/searching.rst @@ -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") + [, ] + + +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) + [, ] + + +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)) + [] + + +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. \ No newline at end of file +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 %} +
+

Editors picks

+ +
+ {% endif %} + {% endwith %} diff --git a/docs/core_components/snippets.rst b/docs/core_components/snippets.rst index 8b2a2d557..33d20bdc4 100644 --- a/docs/core_components/snippets.rst +++ b/docs/core_components/snippets.rst @@ -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 ----------------------------------- diff --git a/docs/editor_manual/index.rst b/docs/editor_manual/index.rst index b7b5c84f7..e7b213903 100644 --- a/docs/editor_manual/index.rst +++ b/docs/editor_manual/index.rst @@ -1,3 +1,5 @@ +.. _editor_manual: + Using Wagtail: an Editor's guide ================================ diff --git a/docs/getting_started/installation.rst b/docs/getting_started/installation.rst index ad161b022..ed246f8af 100644 --- a/docs/getting_started/installation.rst +++ b/docs/getting_started/installation.rst @@ -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 `__ and `VirtualBox `__, 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 you’ve 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 you’ve 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 `__ package manager. We also recommend `virtualenvwrapper `_ 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/ . diff --git a/docs/releases/0.4.rst b/docs/releases/0.4.rst index 4438af87e..4e985e7c2 100644 --- a/docs/releases/0.4.rst +++ b/docs/releases/0.4.rst @@ -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`` diff --git a/docs/releases/0.6.rst b/docs/releases/0.6.rst index 3dc6f92f3..f7da9dc1a 100644 --- a/docs/releases/0.6.rst +++ b/docs/releases/0.6.rst @@ -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 =================== diff --git a/docs/releases/0.7.rst b/docs/releases/0.7.rst index 3878b4d24..8470a746a 100644 --- a/docs/releases/0.7.rst +++ b/docs/releases/0.7.rst @@ -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 =================== diff --git a/docs/releases/index.rst b/docs/releases/index.rst index a9760c239..fce87c573 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -5,6 +5,7 @@ Release notes :maxdepth: 1 roadmap + 0.7 0.6 0.5 0.4.1 diff --git a/requirements-dev.txt b/requirements-dev.txt index 6bc561b0c..82d3e09ab 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,4 @@ coverage==3.7.1 flake8==2.2.1 mock==1.0.1 python-dateutil==2.2 +pytz==2014.7 diff --git a/setup.py b/setup.py index f3a711f5b..eaf2cd10d 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tox.ini b/tox.ini index a3a691f2d..dbb8a5620 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ base = elasticsearch==1.1.0 mock==1.0.1 python-dateutil==2.2 + pytz==2014.7 Embedly coverage diff --git a/wagtail/bin/wagtail.py b/wagtail/bin/wagtail.py index 0987f8084..a7531f5de 100644 --- a/wagtail/bin/wagtail.py +++ b/wagtail/bin/wagtail.py @@ -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 = { diff --git a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html index 667798c80..2929a4506 100644 --- a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html +++ b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html @@ -33,6 +33,7 @@
  • Progress indicators
  • Misc formatters
  • Icons
  • +
  • IE9 debugging
  • @@ -514,6 +515,15 @@ + +
    +

    IE9 debugging

    + +

    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.

    + +
    + +
    {% endblock %} @@ -521,6 +531,30 @@ {% block extra_js %} +{% endblock %} diff --git a/wagtail/wagtailsites/templates/wagtailsites/edit.html b/wagtail/wagtailsites/templates/wagtailsites/edit.html new file mode 100644 index 000000000..c09f06474 --- /dev/null +++ b/wagtail/wagtailsites/templates/wagtailsites/edit.html @@ -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" %} + +
    +
    + {% csrf_token %} + +
      + {% include "wagtailadmin/shared/field_as_li.html" with field=form.hostname %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.port %} + +
    • + {% 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 %} +
    • + + {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_default_site %} + +
    • + + {% if perms.site.delete_site %} + {% trans "Delete site" %} + {% endif %} +
    • +
    +
    +
    +{% endblock %} + +{% block extra_js %} + {% include "wagtailadmin/pages/_editor_js.html" %} + + +{% endblock %} diff --git a/wagtail/wagtailsites/templates/wagtailsites/includes/site_form.js b/wagtail/wagtailsites/templates/wagtailsites/includes/site_form.js new file mode 100644 index 000000000..df3b30993 --- /dev/null +++ b/wagtail/wagtailsites/templates/wagtailsites/includes/site_form.js @@ -0,0 +1,5 @@ +(function() { + function fixPrefix(str) { return str; } + createPageChooser(fixPrefix('id_root_page')); + +})(); diff --git a/wagtail/wagtailsites/templates/wagtailsites/index.html b/wagtail/wagtailsites/templates/wagtailsites/index.html new file mode 100644 index 000000000..9e61ca24c --- /dev/null +++ b/wagtail/wagtailsites/templates/wagtailsites/index.html @@ -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 %} + +
    +
    + + + + + + + + + + + {% for site in sites %} + + + + + + + {% endfor %} + +
    + {% trans "Site" %} + {% if ordering == "site" %} + + {% else %} + + {% endif %} + {% trans "Port" %}{% trans "Root page" %}{% trans "Default?" %}
    +

    + {{ site.hostname }} +

    +
    {{ site.port }}{{ site.root_page }}
    {% if site.is_default_site %}{% trans "Default" %}{% endif %}
    + +
    +
    +{% endblock %} diff --git a/wagtail/wagtailsites/tests.py b/wagtail/wagtailsites/tests.py new file mode 100644 index 000000000..aa061c125 --- /dev/null +++ b/wagtail/wagtailsites/tests.py @@ -0,0 +1,254 @@ +from __future__ import unicode_literals +from django.test import TestCase +from django.core.urlresolvers import reverse + +from wagtail.tests.utils import WagtailTestUtils +from wagtail.wagtailcore.models import Site, Page +import six + + +class TestSiteIndexView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + self.home_page = Page.objects.get(id=2) + + def get(self, params={}): + return self.client.get(reverse('wagtailsites_index'), params) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsites/index.html') + + def test_pagination(self): + pages = ['0', '1', '-1', '9999', 'Not a page'] + for page in pages: + response = self.get({'p': page}) + self.assertEqual(response.status_code, 200) + + +class TestSiteCreateView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + self.home_page = Page.objects.get(id=2) + self.localhost = Site.objects.all()[0] + + def get(self, params={}): + return self.client.get(reverse('wagtailsites_create'), params) + + def post(self, post_data={}): + return self.client.post(reverse('wagtailsites_create'), post_data) + + def create_site(self, hostname='testsite', port=80, is_default_site=False, root_page=None): + root_page = root_page or self.home_page + Site.objects.create( + hostname=hostname, + port=port, + is_default_site=is_default_site, + root_page=root_page) + + def test_default_fixtures_present(self): + # we should have loaded with a single site + self.assertEqual(self.localhost.hostname, 'localhost') + self.assertEqual(self.localhost.port, 80) + self.assertEqual(self.localhost.is_default_site, True) + self.assertEqual(self.localhost.root_page, self.home_page) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsites/create.html') + + def test_create(self): + response = self.post({ + 'hostname': "testsite", + 'port': "80", + 'root_page': str(self.home_page.id), + }) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + + # Check that the site was created + self.assertEqual(Site.objects.filter(hostname='testsite').count(), 1) + + def test_duplicate_defaults_not_allowed(self): + response = self.post({ + 'hostname': "also_default", + 'port': "80", + 'is_default_site': "on", + 'root_page': str(self.home_page.id), + }) + + # Should return the form with errors + self.assertEqual(response.status_code, 200) + self.assertEqual(bool(response.context['form'].errors), True) + + # Check that the site was not created + sites = Site.objects.filter(hostname='also_default') + self.assertEqual(sites.count(), 0) + + def test_duplicate_hostnames_on_different_ports_allowed(self): + response = self.post({ + 'hostname': "localhost", + 'port': "8000", + 'root_page': str(self.home_page.id), + }) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + + # Check that the site was created + self.assertEqual(Site.objects.filter(hostname='localhost').count(), 2) + + def test_duplicate_hostnames_on_same_port_not_allowed(self): + # Confirm there's one localhost already + self.assertEqual(Site.objects.filter(hostname='localhost').count(), 1) + + response = self.post({ + 'hostname': "localhost", + 'port': "80", + 'root_page': str(self.home_page.id), + }) + + # Should return the form with errors + self.assertEqual(response.status_code, 200) + self.assertEqual(bool(response.context['form'].errors), True) + + # Check that the site was not created, still only one localhost entry + self.assertEqual(Site.objects.filter(hostname='localhost').count(), 1) + + +class TestSiteEditView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + self.home_page = Page.objects.get(id=2) + self.localhost = Site.objects.all()[0] + + def get(self, params={}, site_id=None): + return self.client.get(reverse('wagtailsites_edit', args=(site_id or self.localhost.id, )), params) + + def post(self, post_data={}, site_id=None): + site_id = site_id or self.localhost.id + site = Site.objects.get(id=site_id) + post_defaults = { + 'hostname': site.hostname, + 'port': site.port, + 'root_page': site.root_page.id, + } + for k, v in six.iteritems(post_defaults): + post_data[k] = post_data.get(k, v) + if 'default' in post_data: + if post_data['default']: # only include the is_default_site key if we want to set it + post_data['is_default_site'] = 'on' + elif site.is_default_site: + post_data['is_default_site'] = 'on' + return self.client.post(reverse('wagtailsites_edit', args=(site_id,)), post_data) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsites/edit.html') + + def test_nonexistant_redirect(self): + self.assertEqual(self.get(site_id=100000).status_code, 404) + + def test_edit(self): + edited_hostname = 'edited' + response = self.post({ + 'hostname': edited_hostname, + }) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + + # Check that the site was edited + self.assertEqual(Site.objects.get(id=self.localhost.id).hostname, edited_hostname) + + def test_changing_the_default_site_workflow(self): + # First create a second, non-default, site + second_site = Site.objects.create( + hostname="not_yet_default", + port=80, + is_default_site=False, + root_page=self.home_page) + + # Make the original default no longer default + response = self.post( + { + 'default': False, + }, + site_id=self.localhost.id + ) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + # Check that the site is no longer default + self.assertEqual(Site.objects.get(id=self.localhost.id).is_default_site, False) + + # Now make the second site default + response = self.post( + { + 'default': True, + }, + site_id=second_site.id + ) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + # Check that the second site is now set as default + self.assertEqual(Site.objects.get(id=second_site.id).is_default_site, True) + + def test_making_a_second_site_the_default_not_allowed(self): + second_site = Site.objects.create( + hostname="also_default", + port=80, + is_default_site=False, + root_page=self.home_page) + response = self.post( + { + 'default': True, + }, + site_id=second_site.id + ) + + # Should return the form with errors + self.assertEqual(response.status_code, 200) + self.assertEqual(bool(response.context['form'].errors), True) + + # Check that the site was not editd + + self.assertEqual(Site.objects.get(id=second_site.id).is_default_site, False) + + +class TestSiteDeleteView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + self.home_page = Page.objects.get(id=2) + self.localhost = Site.objects.all()[0] + + def get(self, params={}, site_id=None): + return self.client.get(reverse('wagtailsites_delete', args=(site_id or self.localhost.id, )), params) + + def post(self, post_data={}, site_id=None): + return self.client.post(reverse('wagtailsites_delete', args=(site_id or self.localhost.id, )), post_data) + + def test_simple(self): + response = self.get() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsites/confirm_delete.html') + + def test_nonexistant_redirect(self): + self.assertEqual(self.get(site_id=100000).status_code, 404) + + def test_posting_deletes_site(self): + response = self.post({ + 'trivial_key': 'trivial_value' + }) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailsites_index')) + + # Check that the site was edited + with self.assertRaises(Site.DoesNotExist): + Site.objects.get(id=self.localhost.id) diff --git a/wagtail/wagtailsites/urls.py b/wagtail/wagtailsites/urls.py new file mode 100644 index 000000000..96e74a0b2 --- /dev/null +++ b/wagtail/wagtailsites/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url +from wagtail.wagtailsites import views + +urlpatterns = [ + + url(r'^$', views.index, name='wagtailsites_index'), + url(r'^new/$', views.create, name='wagtailsites_create'), + url(r'^(\d+)/$', views.edit, name='wagtailsites_edit'), + url(r'^(\d+)/delete/$', views.delete, name='wagtailsites_delete'), + +] diff --git a/wagtail/wagtailsites/views.py b/wagtail/wagtailsites/views.py new file mode 100644 index 000000000..b7d8b988d --- /dev/null +++ b/wagtail/wagtailsites/views.py @@ -0,0 +1,75 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.contrib.auth.decorators import permission_required, user_passes_test +from django.utils.translation import ugettext as _ + +from wagtail.wagtailcore.models import Site +from wagtail.wagtailsites.forms import SiteForm + + +def user_has_site_model_perm(user): + for verb in ['add', 'change', 'delete']: + if user.has_perm('site.%s_site' % verb): + return True + return False + + +@user_passes_test(user_has_site_model_perm) +def index(request): + sites = Site.objects.all() + return render(request, 'wagtailsites/index.html', { + 'sites': sites, + }) + + +@permission_required('site.add_site') +def create(request): + if request.POST: + form = SiteForm(request.POST) + if form.is_valid(): + site = form.save() + messages.success(request, _("Site '{0}' created.").format(site.hostname)) + return redirect('wagtailsites_index') + else: + messages.error(request, _("The site could not be created due to errors.")) + else: + form = SiteForm() + + return render(request, 'wagtailsites/create.html', { + 'form': form, + }) + + +@permission_required('site.change_site') +def edit(request, site_id): + site = get_object_or_404(Site, id=site_id) + + if request.POST: + form = SiteForm(request.POST, instance=site) + if form.is_valid(): + site = form.save() + messages.success(request, _("Site '{0}' updated.").format(site.hostname)) + return redirect('wagtailsites_index') + else: + messages.error(request, _("The site could not be saved due to errors.")) + else: + form = SiteForm(instance=site) + + return render(request, 'wagtailsites/edit.html', { + 'site': site, + 'form': form, + }) + + +@permission_required('site.delete_site') +def delete(request, site_id): + site = get_object_or_404(Site, id=site_id) + + if request.POST: + site.delete() + messages.success(request, _("Site '{0}' deleted.").format(site.hostname)) + return redirect('wagtailsites_index') + + return render(request, "wagtailsites/confirm_delete.html", { + 'site': site, + }) diff --git a/wagtail/wagtailsites/wagtail_hooks.py b/wagtail/wagtailsites/wagtail_hooks.py new file mode 100644 index 000000000..18e0dbfe2 --- /dev/null +++ b/wagtail/wagtailsites/wagtail_hooks.py @@ -0,0 +1,24 @@ +from django.conf.urls import include, url +from django.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailcore import hooks +from wagtail.wagtailadmin.menu import MenuItem + +from wagtail.wagtailsites import urls + + +@hooks.register('register_admin_urls') +def register_admin_urls(): + return [ + url(r'^sites/', include(urls)), + ] + + +class SitesMenuItem(MenuItem): + def is_shown(self, request): + return request.user.is_superuser + +@hooks.register('register_settings_menu_item') +def register_sites_menu_item(): + return MenuItem(_('Sites'), urlresolvers.reverse('wagtailsites_index'), classnames='icon icon-site', order=602) diff --git a/wagtail/wagtailsnippets/models.py b/wagtail/wagtailsnippets/models.py index 3f1b9cad9..d8d701d03 100644 --- a/wagtail/wagtailsnippets/models.py +++ b/wagtail/wagtailsnippets/models.py @@ -25,6 +25,7 @@ def register_snippet(model): model.usage_url = get_snippet_usage_url SNIPPET_MODELS.append(model) SNIPPET_MODELS.sort(key=lambda x: x._meta.verbose_name) + return model def get_snippet_usage_url(self): diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index 0255925ab..cc0a221f1 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -1,5 +1,6 @@ from django.test import TestCase from django.core.urlresolvers import reverse +from django.db import models from wagtail.tests.utils import WagtailTestUtils from django.test.utils import override_settings @@ -176,6 +177,24 @@ class TestSnippetChooserPanel(TestCase): in self.snippet_chooser_panel.render_js()) +class TestSnippetRegistering(TestCase): + def test_register_function(self): + class RegisterFunction(models.Model): + pass + register_snippet(RegisterFunction) + + self.assertIn(RegisterFunction, SNIPPET_MODELS) + + def test_register_function(self): + @register_snippet + class RegisterDecorator(models.Model): + pass + + # Misbehaving decorators often return None + self.assertIsNotNone(RegisterDecorator) + self.assertIn(RegisterDecorator, SNIPPET_MODELS) + + class TestSnippetOrdering(TestCase): def setUp(self): register_snippet(ZuluSnippet) diff --git a/wagtail/wagtailsnippets/wagtail_hooks.py b/wagtail/wagtailsnippets/wagtail_hooks.py index 06e0c5e56..4c2c8538d 100644 --- a/wagtail/wagtailsnippets/wagtail_hooks.py +++ b/wagtail/wagtailsnippets/wagtail_hooks.py @@ -3,12 +3,14 @@ from django.conf.urls import include, url from django.core import urlresolvers from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import Permission from wagtail.wagtailcore import hooks from wagtail.wagtailadmin.menu import MenuItem from wagtail.wagtailsnippets import urls from wagtail.wagtailsnippets.permissions import user_can_edit_snippets +from wagtail.wagtailsnippets.models import get_snippet_content_types @hooks.register('register_admin_urls') @@ -37,3 +39,10 @@ def editor_js(): 'wagtailsnippets/js/snippet-chooser.js', urlresolvers.reverse('wagtailsnippets_choose_generic') ) + + +@hooks.register('register_permissions') +def register_permissions(): + snippet_content_types = get_snippet_content_types() + snippet_permissions = Permission.objects.filter(content_type__in=snippet_content_types) + return snippet_permissions diff --git a/wagtail/wagtailusers/forms.py b/wagtail/wagtailusers/forms.py index 7c8d0b1b6..e0c8c88c5 100644 --- a/wagtail/wagtailusers/forms.py +++ b/wagtail/wagtailusers/forms.py @@ -2,9 +2,11 @@ from django import forms from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, Permission +from wagtail.wagtailcore import hooks from wagtail.wagtailusers.models import UserProfile -from wagtail.wagtailcore.models import UserPagePermissionsProxy +from wagtail.wagtailcore.models import UserPagePermissionsProxy, GroupPagePermission User = get_user_model() @@ -37,7 +39,7 @@ class UserCreationForm(BaseUserCreationForm): username = self.cleaned_data["username"] try: # When called from BaseUserCreationForm, the method fails if using a AUTH_MODEL_MODEL, - # This is because the following line tries to perform a lookup on + # This is because the following line tries to perform a lookup on # the default "auth_user" table. User._default_manager.get(username=username) except User.DoesNotExist: @@ -136,6 +138,80 @@ class UserEditForm(forms.ModelForm): return user +class GroupForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(GroupForm, self).__init__(*args, **kwargs) + self.registered_permissions = Permission.objects.none() + for fn in hooks.get_hooks('register_permissions'): + self.registered_permissions = self.registered_permissions | fn() + self.fields['permissions'].queryset = self.registered_permissions + + required_css_class = "required" + + error_messages = { + 'duplicate_name': _("A group with that name already exists."), + } + + is_superuser = forms.BooleanField( + label=_("Administrator"), + required=False, + help_text=_("Administrators have the ability to manage user accounts.") + ) + + class Meta: + model = Group + fields = ("name", "permissions", ) + + def clean_name(self): + # Since Group.name is unique, this check is redundant, + # but it sets a nicer error message than the ORM. See #13147. + name = self.cleaned_data["name"] + try: + Group._default_manager.exclude(id=self.instance.id).get(name=name) + except Group.DoesNotExist: + return name + raise forms.ValidationError(self.error_messages['duplicate_name']) + + def save(self): + # We go back to the object to read (in order to reapply) the + # permissions which were set on this group, but which are not + # accessible in the wagtail admin interface, as otherwise these would + # be clobbered by this form. + try: + untouchable_permissions = self.instance.permissions.exclude(pk__in=self.registered_permissions) + bool(untouchable_permissions) # force this to be evaluated, as it's about to change + except ValueError: + # this form is not bound; we're probably creating a new group + untouchable_permissions = [] + group = super(GroupForm, self).save() + group.permissions.add(*untouchable_permissions) + return group + + +class GroupPagePermissionForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(GroupPagePermissionForm, self).__init__(*args, **kwargs) + self.fields['page'].widget = forms.HiddenInput() + + class Meta: + model = GroupPagePermission + fields = ('page', 'permission_type') + + +class BaseGroupPagePermissionFormSet(forms.models.BaseInlineFormSet): + def __init__(self, *args, **kwargs): + super(BaseGroupPagePermissionFormSet, self).__init__(*args, **kwargs) + self.form = GroupPagePermissionForm + for form in self.forms: + form.fields['DELETE'].widget = forms.HiddenInput() + + @property + def empty_form(self): + empty_form = super(BaseGroupPagePermissionFormSet, self).empty_form + empty_form.fields['DELETE'].widget = forms.HiddenInput() + return empty_form + + class NotificationPreferencesForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(NotificationPreferencesForm, self).__init__(*args, **kwargs) diff --git a/wagtail/wagtailusers/models.py b/wagtail/wagtailusers/models.py index eb0aa0c9e..5e63e6af3 100644 --- a/wagtail/wagtailusers/models.py +++ b/wagtail/wagtailusers/models.py @@ -9,19 +9,19 @@ class UserProfile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL) submitted_notifications = models.BooleanField( - default=True, - help_text=_("Receive notification when a page is submitted for moderation") - ) + default=True, + help_text=_("Receive notification when a page is submitted for moderation") + ) approved_notifications = models.BooleanField( - default=True, - help_text=_("Receive notification when your page edit is approved") - ) + default=True, + help_text=_("Receive notification when your page edit is approved") + ) rejected_notifications = models.BooleanField( - default=True, - help_text=_("Receive notification when your page edit is rejected") - ) + default=True, + help_text=_("Receive notification when your page edit is rejected") + ) @classmethod def get_for_user(cls, user): diff --git a/wagtail/wagtailusers/static/wagtailusers/scss/groups_edit.scss b/wagtail/wagtailusers/static/wagtailusers/scss/groups_edit.scss new file mode 100644 index 000000000..85a13f071 --- /dev/null +++ b/wagtail/wagtailusers/static/wagtailusers/scss/groups_edit.scss @@ -0,0 +1,9 @@ +.listing { + .field label{ + display:none; + } + input,select,textarea{ + font-size:1em; + } + +} \ No newline at end of file diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/confirm_delete.html b/wagtail/wagtailusers/templates/wagtailusers/groups/confirm_delete.html new file mode 100644 index 000000000..c86ab250f --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/confirm_delete.html @@ -0,0 +1,33 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Delete group" %}{% endblock %} +{% block bodyclass %}menu-groups{% endblock %} + +{% block content %} + {% trans "Delete group" as del_str %} + {% include "wagtailadmin/shared/header.html" with title=del_str subtitle=group.name icon="group" %} + +
    +
    +

    + {% blocktrans with group_user_count=group.user_set.count group_name=group.name %}The group '{{ group_name }}' has {{ group_user_count }} users assigned.{% endblocktrans %} +

    +

    + {% if group.user_set.count %} + If you delete the group, those users will lose the permissions + granted to them by membership of this group, but retain + permissions they have through membership of other groups. + {% else %} + It is probably safe to delete. + {% endif %} +

    +
    +
    +

    {% trans "Are you sure you want to delete this group?" %}

    +
    + {% csrf_token %} + +
    +
    +
    +{% endblock %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/create.html b/wagtail/wagtailusers/templates/wagtailusers/groups/create.html new file mode 100644 index 000000000..7cc64cda4 --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/create.html @@ -0,0 +1,44 @@ +{% extends "wagtailadmin/base.html" %} +{% load wagtailusers_tags wagtailimages_tags %} +{% load i18n %} +{% load compress %} + +{% block titletag %}{% trans "Add group" %}{% endblock %} +{% block bodyclass %}menu-groups{% endblock %} + +{% block extra_css %} + {% compress css %} + + {% endcompress %} +{% endblock %} + +{% block content %} + + {% trans "Add group" as add_group_str %} + {% include "wagtailadmin/shared/header.html" with title=add_group_str icon="group" %} + +
    +
    + {% csrf_token %} + +
      + {% include "wagtailadmin/shared/field_as_li.html" with field=form.name %} +
    • + {% format_permissions permission_bound_field=form.permissions %} +
    • +
    • + {% include "wagtailusers/groups/includes/page_permissions_formset.html" with formset=formset only %} +
    • +
    • +
    +
    +
    +{% endblock %} + +{% block extra_js %} + {% include "wagtailadmin/pages/_editor_js.html" %} + + +{% endblock %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/edit.html b/wagtail/wagtailusers/templates/wagtailusers/groups/edit.html new file mode 100644 index 000000000..5b836e851 --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/edit.html @@ -0,0 +1,52 @@ +{% extends "wagtailadmin/base.html" %} +{% load wagtailusers_tags wagtailimages_tags %} +{% load i18n %} +{% load compress %} + +{% block titletag %}{% trans "Editing" %} {{ group.name }}{% endblock %} +{% block bodyclass %}menu-groups{% endblock %} + +{% block extra_css %} + {% compress css %} + + {% endcompress %} +{% endblock %} + +{% block content %} + + {% trans "Editing" as editing_str %} + {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=group.name icon="group" %} + +
    +
    + {% csrf_token %} + +
      + {% include "wagtailadmin/shared/field_as_li.html" with field=form.name %} + +
    • + {% format_permissions permission_bound_field=form.permissions %} +
    • +
    • + {% include "wagtailusers/groups/includes/page_permissions_formset.html" with formset=formset only %} +
    • +
    • + + + {% if perms.auth.delete_group %} + {% trans "Delete group" %} + {% endif %} +
    • +
    + +
    +
    +{% endblock %} + +{% block extra_js %} + {% include "wagtailadmin/pages/_editor_js.html" %} + + +{% endblock %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/includes/formatted_permissions.html b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/formatted_permissions.html new file mode 100644 index 000000000..b3f46e1ec --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/formatted_permissions.html @@ -0,0 +1,63 @@ +{% load i18n %} + +

    {% trans "Object permissions" %}

    + + + + + + + + + + + + + + + {% for content_perms_dict in object_perms %} + + + + + + + {% endfor %} + +
    {% trans "Name" %}{% trans "Add" %}{% trans "Change" %}{% trans "Delete" %}

    {{ content_perms_dict.object|capfirst }}

    + {% with content_perms_dict.add as perm_tuple %} + + {% endwith %} + + {% with content_perms_dict.change as perm_tuple %} + + {% endwith %} + + {% with content_perms_dict.delete as perm_tuple %} + + {% endwith %} +
    + +

    {% trans "Other permissions" %}

    + + + + + + + + + + + {% for perm_tuple in other_perms %} + + + + + {% endfor %} + +
    {% trans "Name" %}
    {{ perm_tuple.0.name }} + +
    diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_form.html b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_form.html new file mode 100644 index 000000000..2521b11fe --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_form.html @@ -0,0 +1,24 @@ +{% load i18n %} + + + {% trans "Edit page" as choose_another_text_str %} + {% trans "Choose 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 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 choose_one_text_str=choose_one_text_str choose_another_text_str=choose_another_text_str only %} + {% endif %} + + + {% include "wagtailadmin/edit_handlers/field_panel_field.html" with field=form.permission_type only %} + + {{ form.id }} + {{ form.ORDER }} + {{ form.DELETE }} + + + + + + diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.html b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.html new file mode 100644 index 000000000..98a60a8c3 --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.html @@ -0,0 +1,42 @@ +{% load i18n %} +

    Page permissions

    + +{{ formset.management_form }}{# what is this? #} + + + + + + + + + + + + + + {% for form in formset.forms %} + + {% if form.non_field_errors %} +

    + {% for error in form.non_field_errors %} + {{ error|escape }} + {% endfor %} +

    + {% endif %} + {% include "wagtailusers/groups/includes/page_permissions_form.html" with form=form only %} + + {% endfor %} + +
    PagePermission type
    + + + +

    + {% trans "Add a page permission" %} +

    + diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js new file mode 100644 index 000000000..c9c93a48b --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js @@ -0,0 +1,19 @@ +(function() { + function fixPrefix(str) {return str;} + + var panel = InlinePanel({ + formsetPrefix: fixPrefix("id_{{ formset.prefix }}"), + emptyChildFormPrefix: fixPrefix("{{ formset.empty_form.prefix }}"), + + onAdd: function(fixPrefix) { + createPageChooser(fixPrefix('id_{{ formset.prefix }}-__prefix__-page'), 'wagtailcore.page', null); + } + }); + + {% for form in formset.forms %} + createPageChooser(fixPrefix('id_{{ formset.prefix }}-{{ forloop.counter0 }}-page'), 'wagtailcore.page', null); + panel.initChildControls('{{ formset.prefix }}-{{ forloop.counter0 }}'); + {% endfor %} + + panel.updateMoveButtonDisabledStates(); +})(); diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/index.html b/wagtail/wagtailusers/templates/wagtailusers/groups/index.html new file mode 100644 index 000000000..45934be39 --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/index.html @@ -0,0 +1,26 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% load gravatar %} +{% block titletag %}{% trans "groups" %}{% endblock %} +{% block bodyclass %}menu-groups{% endblock %} +{% block extra_js %} + +{% endblock %} + +{% block content %} + {% trans "groups" as groups_str %} + {% trans "Add a group" as add_a_group_str %} + {% include "wagtailadmin/shared/header.html" with title=groups_str add_link="wagtailusers_groups_create" add_text=add_a_group_str icon="group" search_url="wagtailusers_groups_index" %} + +
    +
    + {% include "wagtailusers/groups/results.html" %} +
    +
    +{% endblock %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/list.html b/wagtail/wagtailusers/templates/wagtailusers/groups/list.html new file mode 100644 index 000000000..e72fc554f --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/list.html @@ -0,0 +1,26 @@ +{% load i18n %} + + + + + + + + {% for group in groups %} + + + + {% endfor %} + +
    + {% trans "Name" %} + {% if ordering == "name" %} + + {% else %} + + {% endif %} +
    +

    + {{ group.name }} +

    +
    diff --git a/wagtail/wagtailusers/templates/wagtailusers/groups/results.html b/wagtail/wagtailusers/templates/wagtailusers/groups/results.html new file mode 100644 index 000000000..ea3cc5650 --- /dev/null +++ b/wagtail/wagtailusers/templates/wagtailusers/groups/results.html @@ -0,0 +1,10 @@ +{% load i18n %} +{% if groups %} + + {% include "wagtailusers/groups/list.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=groups is_searching=is_searching linkurl="wagtailusers_groups_index" %} +{% else %} + {% url 'wagtailusers_groups_create' as wagtailusers_create_group_url %} +

    {% blocktrans %}There are no groups configured. Why not add some?{% endblocktrans %}

    +{% endif %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/create.html b/wagtail/wagtailusers/templates/wagtailusers/users/create.html similarity index 96% rename from wagtail/wagtailusers/templates/wagtailusers/create.html rename to wagtail/wagtailusers/templates/wagtailusers/users/create.html index 91cba6846..02e7ecec1 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/create.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/create.html @@ -13,7 +13,7 @@
  • {% trans "Roles" %}
  • -
    +
    {% csrf_token %}
    diff --git a/wagtail/wagtailusers/templates/wagtailusers/edit.html b/wagtail/wagtailusers/templates/wagtailusers/users/edit.html similarity index 96% rename from wagtail/wagtailusers/templates/wagtailusers/edit.html rename to wagtail/wagtailusers/templates/wagtailusers/users/edit.html index 1f0cc384f..22ae8b3a7 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/edit.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/edit.html @@ -13,7 +13,7 @@
  • {% trans "Roles" %}
  • - +
    {% csrf_token %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/index.html b/wagtail/wagtailusers/templates/wagtailusers/users/index.html similarity index 73% rename from wagtail/wagtailusers/templates/wagtailusers/index.html rename to wagtail/wagtailusers/templates/wagtailusers/users/index.html index d2f3e9972..67589aaaf 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/index.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/index.html @@ -6,7 +6,7 @@ {% block extra_js %}