diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8f391a29a..a0abb58db 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,14 +1,16 @@ Changelog ========= -0.4 (xx.xx.20xx) +0.4 (10.07.2014) ~~~~~~~~~~~~~~~~ * ElasticUtils/pyelasticsearch swapped for elasticsearch-py * Python 3.2, 3.3 and 3.4 support * Added scheduled publishing + * Added support for private (password-protected) pages * Added frontend cache invalidator * Added sitemap generator * Added notification preferences + * Added a new way to configure searchable/filterable fields on models * Added 'original' as a resizing rule supported by the 'image' tag * Hallo.js updated to version 1.0.4 * Snippets are now ordered alphabetically @@ -29,6 +31,14 @@ Changelog * Added init_new_page signal * Added page_published signal * Added copy method to Page to allow copying of pages + * Added ``search`` method to ``PageQuerySet`` + * Added ``get_indexed_objects`` allowing developers to customise which objects get added to the search index + * Major refactor of Elasticsearch backend + * Use ``match`` instead of ``query_string`` queries + * Fields are now indexed in Elasticsearch with their correct type + * Filter fields are no longer included in '_all' (in Elasticsearch) + * Fields with partial matching are now indexed together into '_partials' + * Fix: Animated GIFs are now coalesced before resizing * Fix: Wand backend clones images before modifying them * Fix: Admin breadcrumb now positioned correctly on mobile diff --git a/README.rst b/README.rst index f0dc14abf..5fa7f47f9 100644 --- a/README.rst +++ b/README.rst @@ -55,7 +55,8 @@ Contributing ~~~~~~~~~~~~ If you're a Python or Django developer, fork the repo and get stuck in! -We suggest you start by checking the `Help develop me! `_ label. +We suggest you start by checking the `Help develop me! `_ label and the `coding guidelines `_. Send us a useful pull request and we'll post you a `t-shirt `_. +We also welcome `translations `_ for Wagtail's interface. diff --git a/docs/building_your_site/djangodevelopers.rst b/docs/building_your_site/djangodevelopers.rst index aeabb264f..ee066b99a 100644 --- a/docs/building_your_site/djangodevelopers.rst +++ b/docs/building_your_site/djangodevelopers.rst @@ -171,15 +171,18 @@ Other Relationships Your ``Page``-derived models might have other interrelationships which extend the basic Wagtail tree or depart from it entirely. You could provide functions to navigate between siblings, such as a "Next Post" link on a blog page (``post->post->post``). It might make sense for subtrees to interrelate, such as in a discussion forum (``forum->post->replies``) Skipping across the hierarchy might make sense, too, as all objects of a certain model class might interrelate regardless of their ancestors (``events = EventPage.objects.all``). It's largely up to the models to define their interrelations, the possibilities are really endless. +.. _anatomy_of_a_wagtail_request: + Anatomy of a Wagtail Request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For going beyond the basics of model definition and interrelation, it might help to know how Wagtail handles requests and constructs responses. In short, it goes something like: #. Django gets a request and routes through Wagtail's URL dispatcher definitions - #. Starting from the root content piece, Wagtail traverses the page tree, letting the model for each piece of content along the path decide how to ``route()`` the next step in the path. - #. A model class decides that routing is done and it's now time to ``serve()`` content. - #. ``serve()`` constructs a context using ``get_context()`` + #. Wagtail checks the hostname of the request to determine which ``Site`` record will handle this request. + #. Starting from the root page of that site, Wagtail traverses the page tree, calling the ``route()`` method and letting each page model decide whether it will handle the request itself or pass it on to a child page. + #. The page responsible for handling the request returns a ``RouteResult`` object from ``route()``, which identifies the page along with any additional args/kwargs to be passed to ``serve()``. + #. Wagtail calls ``serve()``, which constructs a context using ``get_context()`` #. ``serve()`` finds a template to pass it to using ``get_template()`` #. A response object is returned by ``serve()`` and Django responds to the requester. @@ -201,6 +204,7 @@ Properties: * status_string * subpage_types * indexed_fields +* preview_modes Methods: @@ -213,8 +217,7 @@ Methods: * get_descendants * get_siblings * search -* get_page_modes -* show_as_mode +* serve_preview Page Queryset Methods diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 35aa1c845..a5120af19 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -192,8 +192,9 @@ 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 -**Adding attributes to the {% image %} tag** Extra attributes can be specified with the syntax ``attribute="value"``: @@ -201,25 +202,31 @@ Extra attributes can be specified with the syntax ``attribute="value"``: {% image self.photo width-400 class="foo" id="bar" %} -No validation is performed on attributes add in this way by the developer. It's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself. +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. -**Generating the image "as"** +**2. Generating the image "as foo" to access individual properties** -Wagtail can assign the image data to another object using Django's ``as`` syntax: +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 }} + + +.. Note:: + The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``. + -.. versionadded:: 0.4 The ``attrs`` shortcut ----------------------- -You can also use the ``attrs`` property as a shorthand to output the ``src``, ``width``, ``height`` and ``alt`` attributes in one go: +.. 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 diff --git a/docs/conf.py b/docs/conf.py index 11517ada2..d54c30df7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Wagtail Documentation' +project = u'Wagtail' copyright = u'2014, Torchbox' # The version info for the project you're documenting, acts as replacement for @@ -59,9 +59,9 @@ copyright = u'2014, Torchbox' # built documents. # # The short X.Y version. -version = '0.1' +version = '0.4' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contributing.rst b/docs/contributing.rst index f5eb168b2..797176997 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -15,6 +15,7 @@ Coding guidelines ~~~~~~~~~~~~~~~~~ * PEP8. We ask that all Python contributions adhere to the `PEP8 `_ style guide, apart from the restriction on line length (E501). The `pep8 tool `_ makes it easy to check your code, e.g. ``pep8 --ignore=E501 your_file.py``. +* Python 2 and 3 compatibility. All contributions should support Python 2 and 3 and we recommend using the `six `_ compatibility library (use the pip version installed as a dependency, not the version bundled with Django). * Tests. Wagtail has a suite of tests, which we are committed to improving and expanding. We run continuous integration at `travis-ci.org/torchbox/wagtail `_ to ensure that no commits or pull requests introduce test failures. If your contributions add functionality to Wagtail, please include the additional tests to cover it; if your contributions alter existing functionality, please update the relevant tests accordingly. Styleguide diff --git a/docs/editing_api.rst b/docs/editing_api.rst index 5113eb3b8..48d766675 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -1,6 +1,6 @@ Defining models with the Editing API -=========== +==================================== .. note:: This documentation is currently being written. @@ -251,7 +251,7 @@ Field Customization By adding CSS classnames to your panel definitions or adding extra parameters to your field definitions, you can control much of how your fields will display in the Wagtail page editing interface. Wagtail's page editing interface takes much of its behavior from Django's admin, so you may find many options for customization covered there. (See `Django model field reference`_ ). -.. _Django model field reference:https://docs.djangoproject.com/en/dev/ref/models/fields/ +.. _Django model field reference: https://docs.djangoproject.com/en/dev/ref/models/fields/ Full-Width Input @@ -374,7 +374,7 @@ For more on ``django-modelcluster``, visit `the django-modelcluster github proje Extending the WYSIWYG Editor (hallo.js) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js` hook. Once you have the hook in place and your hallo.js plugin loads into the Wagtail page editor, use the following Javascript to register the plugin with hallo.js. +To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js ` hook. Once you have the hook in place and your hallo.js plugin loads into the Wagtail page editor, use the following Javascript to register the plugin with hallo.js. .. code-block:: javascript @@ -402,6 +402,23 @@ Registering functions with a Wagtail hook follows the following pattern: Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook. +.. _before_serve_page: + +``before_serve_page`` + .. versionadded:: 0.4 + + Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the args and kwargs that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page. + + .. code-block:: python + + from wagtail.wagtailcore import hooks + + def block_googlebot(page, request, serve_args, serve_kwargs): + if request.META.get('HTTP_USER_AGENT') == 'GoogleBot': + return HttpResponse("

bad googlebot no cookie

") + + hooks.register('before_serve_page', block_googlebot) + .. _construct_wagtail_edit_bird: ``construct_wagtail_edit_bird`` @@ -565,7 +582,8 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func .. _construct_whitelister_element_rules: ``construct_whitelister_element_rules`` - .. versionadded:: 0.4 +.. versionadded:: 0.4 + Customise the rules that define which HTML elements are allowed in rich text areas. By default only a limited set of HTML elements and attributes are whitelisted - all others are stripped out. The callables passed into this hook must return a dict, which maps element names to handler functions that will perform some kind of manipulation of the element. These handler functions receive the element as a `BeautifulSoup `_ Tag object. The ``wagtail.wagtailcore.whitelist`` module provides a few helper functions to assist in defining these handlers: ``allow_without_attributes``, a handler which preserves the element but strips out all of its attributes, and ``attribute_rule`` which accepts a dict specifying how to handle each attribute, and returns a handler function. This dict will map attribute names to either True (indicating that the attribute should be kept), False (indicating that it should be dropped), or a callable (which takes the initial attribute value and returns either a final value for the attribute, or None to drop the attribute). @@ -593,6 +611,7 @@ On loading, Wagtail will search for any app with the file ``image_formats.py`` a As an example, add a "thumbnail" format: .. code-block:: python + # image_formats.py from wagtail.wagtailimages.formats import Format, register_image_format diff --git a/docs/editor_manual/documents_images_snippets/index.rst b/docs/editor_manual/documents_images_snippets/index.rst index 13ceb039e..35817aa79 100644 --- a/docs/editor_manual/documents_images_snippets/index.rst +++ b/docs/editor_manual/documents_images_snippets/index.rst @@ -10,3 +10,4 @@ Wagtail allows you to manage all of your documents and images through their own documents images + snippets diff --git a/docs/editor_manual/documents_images_snippets/snippets.rst b/docs/editor_manual/documents_images_snippets/snippets.rst index dd9fbccc0..0dfa1fdad 100644 --- a/docs/editor_manual/documents_images_snippets/snippets.rst +++ b/docs/editor_manual/documents_images_snippets/snippets.rst @@ -1,6 +1,9 @@ Snippets ~~~~~~~~ +.. note:: + Documentation currently incomplete and in draft status + .. UNSURE HOW TO WRITE THIS AS THE ADVERT EXAMPLE IN WAGTAIL DEMO IS NOT A PARTICULARLY HELPFUL USE CASE. When creating a page on a website, it is a common occurance to want to add in a piece of content that already exists on another page. An example of this would be a person's contact details, or an advert that you want to simply show on every page of your site, without having to manually apply it. diff --git a/docs/frontend_cache_purging.rst b/docs/frontend_cache_purging.rst index a949bdc6e..c1b03624f 100644 --- a/docs/frontend_cache_purging.rst +++ b/docs/frontend_cache_purging.rst @@ -1,3 +1,5 @@ +.. _frontend_cache_purging: + Frontend cache purging ====================== @@ -100,3 +102,18 @@ Let's take the the above BlogIndexPage as an example. We need to register a sign @register(pre_delete, sender=BlogPage) def blog_deleted_handler(instance): blog_page_changed(instance) + + +Purging individual URLs +----------------------- + +``wagtail.contrib.wagtailfrontendcache.utils`` provides another utils function called ``purge_url_from_cache``. As the name suggests, this purges an individual URL from the cache. + +For example, this could be useful for purging a single page of blogs: + +.. code-block:: python + + from wagtail.contrib.wagtailfrontendcache.utils import purge_url_from_cache + + # Purge the first page of the blog index + purge_url_from_cache(blog_index.url + '?page=1') diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index de635256c..04905700e 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -40,7 +40,7 @@ Once you've experimented with the demo app and are ready to build your pages via On OS X ~~~~~~~ -Install `pip `_ and `virtualenvwrapper `_ if you don't have them already. Then, in your terminal:: +Install `pip `__ and `virtualenvwrapper `_ if you don't have them already. Then, in your terminal:: mkvirtualenv wagtaildemo git clone https://github.com/torchbox/wagtaildemo.git @@ -75,10 +75,10 @@ Wagtail instance available as the basis for your new site: ./manage.py runserver 0.0.0.0:8000 - This will make the app accessible on the host machine as -`localhost:8111 `_ - you can access the Wagtail admin -interface at `localhost:8111/admin `_. The codebase -is located on the host machine, exported to the VM as a shared folder; code -editing and Git operations will generally be done on the host. + `localhost:8111 `_ - you can access the Wagtail admin + interface at `localhost:8111/admin `_. The codebase + is located on the host machine, exported to the VM as a shared folder; code + editing and Git operations will generally be done on the host. Using Docker ~~~~~~~~~~~~ @@ -101,7 +101,7 @@ use the following steps: Required dependencies ===================== -- `pip `_ +- `pip `__ - `libjpeg `_ - `libxml2 `_ - `libxslt `_ diff --git a/docs/index.rst b/docs/index.rst index ae2300556..2ab429eb6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,15 +13,19 @@ It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 suppo building_your_site/index editing_api snippets - wagtail_search + search/index form_builder model_recipes advanced_topics deploying performance + private_pages static_site_generation + frontend_cache_purging + sitemap_generation management_commands contributing support roadmap editor_manual/index + releases/index diff --git a/docs/management_commands.rst b/docs/management_commands.rst index 3f77fc3ef..18358a869 100644 --- a/docs/management_commands.rst +++ b/docs/management_commands.rst @@ -1,6 +1,11 @@ +.. _management_commands: + Management commands =================== + +.. _publish_scheduled_pages: + publish_scheduled_pages ----------------------- @@ -8,6 +13,9 @@ publish_scheduled_pages This command publishes or unpublishes pages that have had these actions scheduled by an editor. It is recommended to run this command once an hour. + +.. _fixtree: + fixtree ------- @@ -15,6 +23,9 @@ fixtree This command scans for errors in your database and attempts to fix any issues it finds. + +.. _move_pages: + move_pages ---------- @@ -25,10 +36,13 @@ This command moves a selection of pages from one section of the tree to another. Options: - **from** - This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children. + This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children. - **to** - This is the **id** of the page to move pages to. + This is the **id** of the page to move pages to. + + +.. _update_index: update_index ------------ @@ -44,6 +58,9 @@ It is recommended to run this command once a week and at the following times: The search may not return any results while this command is running, so avoid running it at peak times. + +.. _search_garbage_collect: + search_garbage_collect ---------------------- diff --git a/docs/model_recipes.rst b/docs/model_recipes.rst index 6a4abed47..b8f006f99 100644 --- a/docs/model_recipes.rst +++ b/docs/model_recipes.rst @@ -38,6 +38,8 @@ Consider this example from the Wagtail demo site's ``models.py``, which serves a With this strategy, you could use Django or Python utilities to render your model in JSON or XML or any other format you'd like. +.. _overriding_route_method: + Adding Endpoints with Custom route() Methods -------------------------------------------- @@ -58,8 +60,8 @@ Wagtail routes requests by iterating over the path components (separated with a # find a matching child or 404 try: subpage = self.get_children().get(slug=child_slug) - except Page.DoesNotExist: - raise Http404 + except Page.DoesNotExist: + raise Http404 # delegate further routing return subpage.specific.route(request, remaining_components) @@ -67,13 +69,16 @@ Wagtail routes requests by iterating over the path components (separated with a else: # request is for this very page if self.live: - # use the serve() method to render the request if the page is published - return self.serve(request) + # Return a RouteResult that will tell Wagtail to call + # this page's serve() method + return RouteResult(self) else: # the page matches the request, but isn't published, so 404 raise Http404 -The contract is pretty simple. ``route()`` takes the current object (``self``), the ``request`` object, and a list of the remaining ``path_components`` from the request URL. It either continues delegating routing by calling ``route()`` again on one of its children in the Wagtail tree, or ends the routing process by serving something -- either normally through the ``self.serve()`` method or by raising a 404 error. +``route()`` takes the current object (``self``), the ``request`` object, and a list of the remaining ``path_components`` from the request URL. It either continues delegating routing by calling ``route()`` again on one of its children in the Wagtail tree, or ends the routing process by returning a ``RouteResult`` object or raising a 404 error. + +The ``RouteResult`` object (defined in wagtail.wagtailcore.url_routing) encapsulates all the information Wagtail needs to call a page's ``serve()`` method and return a final response: this information consists of the page object, and any additional args / kwargs to be passed to ``serve()``. By overriding the ``route()`` method, we could create custom endpoints for each object in the Wagtail tree. One use case might be using an alternate template when encountering the ``print/`` endpoint in the path. Another might be a REST API which interacts with the current object. Just to see what's involved, lets make a simple model which prints out all of its child path components. @@ -82,6 +87,7 @@ First, ``models.py``: .. code-block:: python from django.shortcuts import render + from wagtail.wagtailcore.url_routing import RouteResult ... @@ -89,15 +95,20 @@ First, ``models.py``: def route(self, request, path_components): if path_components: - return render(request, self.template, { - 'self': self, - 'echo': ' '.join(path_components), - }) + # tell Wagtail to call self.serve() with an additional 'path_components' kwarg + return RouteResult(self, kwargs={'path_components': path_components}) else: if self.live: - return self.serve(request) - else: - raise Http404 + # tell Wagtail to call self.serve() with no further args + return RouteResult(self) + else: + raise Http404 + + def serve(self, path_components=[]): + render(request, self.template, { + 'self': self, + 'echo': ' '.join(path_components), + }) Echoer.content_panels = [ FieldPanel('title', classname="full title"), @@ -107,7 +118,7 @@ First, ``models.py``: MultiFieldPanel(COMMON_PANELS, "Common page configuration"), ] -This model, ``Echoer``, doesn't define any properties, but does subclass ``Page`` so objects will be able to have a custom title and slug. The template just has to display our ``{{ echo }}`` property. We're skipping the ``serve()`` method entirely, but you could include your render code there to stay consistent with Wagtail's conventions. +This model, ``Echoer``, doesn't define any properties, but does subclass ``Page`` so objects will be able to have a custom title and slug. The template just has to display our ``{{ echo }}`` property. Now, once creating a new ``Echoer`` page in the Wagtail admin titled "Echo Base," requests such as:: @@ -117,6 +128,12 @@ Will return:: tauntaun kennel bed and breakfast +Be careful if you're introducing new required arguments to the ``serve()`` method - Wagtail still needs to be able to display a default view of the page for previewing and moderation, and by default will attempt to do this by calling ``serve()`` with a request object and no further arguments. If your ``serve()`` method does not accept that as a method signature, you will need to override the page's ``serve_preview()`` method to call ``serve()`` with suitable arguments: + +.. code-block:: python + + def serve_preview(self, request, mode_name): + return self.serve(request, color='purple') .. _tagging: @@ -190,11 +207,11 @@ Load Alternate Templates by Overriding get_template() -Page Modes ----------- +Preview Modes +------------- -get_page_modes -show_as_mode +preview_modes +serve_preview diff --git a/docs/performance.rst b/docs/performance.rst index 2f4a834e0..09f06be0f 100644 --- a/docs/performance.rst +++ b/docs/performance.rst @@ -1,7 +1,7 @@ Performance =========== -Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eeking out the most from your installation. +Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation. Editor interface ~~~~~~~~~~~~~~~~ @@ -41,4 +41,4 @@ Public users Caching proxy ------------- -To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish `_ and `Squid `_ have been tested in production. Hosted proxies like `Cloudflare `_ should also work well. \ No newline at end of file +To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish `_ and `Squid `_ have been tested in production. Hosted proxies like `Cloudflare `_ should also work well. diff --git a/docs/private_pages.rst b/docs/private_pages.rst new file mode 100644 index 000000000..99b364176 --- /dev/null +++ b/docs/private_pages.rst @@ -0,0 +1,64 @@ +.. _private_pages: + +Private pages +============= + +Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface, and setting a password. Users visiting this page, or any of its subpages, will be prompted to enter a password before they can view the page. + +Private pages work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "password required" form is only a bare-bones HTML page, and site implementers may wish to replace this with a page customised to their site design. + +Setting up a global "password required" page +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By setting ``PASSWORD_REQUIRED_TEMPLATE`` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below): + +.. code-block:: python + + PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html' + +This template will receive the same set of context variables that the blocked page would pass to its own template via ``get_context()`` - including ``self`` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name): + + - **form** - A Django form object for the password prompt; this will contain a field named ``password`` as its only visible field. A number of hidden fields may also be present, so the page must loop over ``form.hidden_fields`` if not using one of Django's rendering helpers such as ``form.as_p``. + - **action_url** - The URL that the password form should be submitted to, as a POST request. + +A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look like this: + + .. code-block:: django + + + + + Password required + + +

Password required

+

You need a password to access this page.

+
+ {% csrf_token %} + + {{ form.non_field_errors }} + +
+ {{ form.password.errors }} + {{ form.password.label_tag }} + {{ form.password }} +
+ + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + +
+ + + +Setting a "password required" page for a specific page type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The attribute ``password_required_template`` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed. + + .. code-block:: python + + class VideoPage(Page): + ... + password_required_template = 'video/password_required.html' diff --git a/docs/releases/0.4.rst b/docs/releases/0.4.rst new file mode 100644 index 000000000..0929935f6 --- /dev/null +++ b/docs/releases/0.4.rst @@ -0,0 +1,205 @@ +========================= +Wagtail 0.4 release notes +========================= + +.. contents:: + :local: + :depth: 1 + + +What's new +========== + + +Private Pages +~~~~~~~~~~~~~ + +Wagtail now supports password protecting pages on the frontend, allowing sections of your website to be made private. + +:ref:`private_pages` + + +Python 3 support +~~~~~~~~~~~~~~~~ + +Wagtail now supports Python 3.2, 3.3 and 3.4. + + +Scheduled publishing +~~~~~~~~~~~~~~~~~~~~ + +Editors can now schedule pages to be published or unpublished at specified times. + +A new management command has been added (:ref:`publish_scheduled_pages`) to publish pages that have been scheduled by an editor. + + +Search on QuerySet with Elasticsearch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's now possible to perform searches with Elasticsearch on ``PageQuerySet`` objects: + + >>> from wagtail.wagtailcore.models import Page + >>> Page.objects.live().descendant_of(events_index).search("Hello") + [, ] + + +Sitemap generation +~~~~~~~~~~~~~~~~~~ + +A new module has been added (``wagtail.contrib.wagtailsitemaps``) which produces XML sitemaps for Wagtail sites. + +:ref:`sitemap_generation` + + +Front-end cache invalidation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new module has been added (``wagtail.contrib.wagtailfrontendcache``) which invalidates pages in a frontend cache when they are updated or deleted in Wagtail. + +:ref:`frontend_cache_purging` + + +Notification preferences +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can now decide which notifications they receive from Wagtail using a new "Notification preferences" section located in the account settings. + + +Minor features +~~~~~~~~~~~~~~ + + +Core +---- + + * Any extra arguments given to ``Page.serve`` are now passed through to ``get_context`` and ``get_template`` + * Added ``in_menu`` and ``not_in_menu`` methods to ``PageQuerySet`` + * Added ``search`` method to ``PageQuerySet`` + * Added ``get_next_siblings`` and ``get_prev_siblings`` to ``Page`` + * Added ``page_published`` signal + * Added ``copy`` method to ``Page`` to allow copying of pages + * Added ``construct_whitelister_element_rules`` hook for customising the HTML whitelist used when saving ``RichText`` fields + * Support for setting a ``subpage_types`` property on ``Page`` models, to define which page types are allowed as subpages + + +Admin +----- + + * Removed the "More" section from the menu + * Added pagination to page listings + * Added a new datetime picker widget + * Updated hallo.js to version 1.0.4 + * Aesthetic improvements to preview experience + * Login screen redirects to dashboard if user is already logged in + * Snippets are now ordered alphabetically + * Added ``init_new_page`` signal + + +Search +------ + + * Added a new way to configure searchable/filterable fields on models + * Added ``get_indexed_objects`` allowing developers to customise which objects get added to the search index + * Major refactor of Elasticsearch backend + * Use ``match`` instead of ``query_string`` queries + * Fields are now indexed in Elasticsearch with their correct type + * Filter fields are no longer included in '_all' + * Fields with partial matching are now indexed together into '_partials' + + +Images +------ + + * Added ``original`` as a resizing rule supported by the ``{% image %}`` tag + * ``image`` tag now accepts extra keyword arguments to be output as attributes on the img tag + * Added an ``attrs`` property to image rendition objects to output ``src``, ``width``, ``height`` and ``alt`` attributes all in one go + + +Other +----- + + * Added styleguide, for Wagtail developers + + +Bug fixes +~~~~~~~~~ + + * Animated GIFs are now coalesced before resizing + * The Wand backend clones images before modifying them + * The admin breadcrumb is now positioned correctly on mobile + * The page chooser breadcrumb now updates the chooser modal instead of linking to Explorer + * Embeds - fixed crash when no HTML field is sent back from the embed provider + * Multiple sites with same hostname but different ports are now allowed + * It is no longer possible to create multiple sites with ``is_default_site = True`` + + +Backwards-incompatible changes +============================== + + +ElasticUtils replaced with elasticsearch-py +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are using the elasticsearch backend, you must install the ``elasticsearch`` module into your environment. + + +.. note:: + + If you are using an older version of Elasticsearch (< 1.0) you must install ``elasticsearch`` version 0.4.x. + + +Addition of ``expired`` column may break old data migrations involving pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The scheduled publishing mechanism adds an ``expired`` field to wagtailcore.Page, defaulting to False. Any application code working with Page objects should be unaffected, but any code that creates page records using direct SQL, or within existing South migrations using South's frozen ORM, will fail as this code will be unaware of the ``expired`` database column. To fix a South migration that fails in this way, add the following line to the ``'wagtailcore.page'`` entry at the bottom of the migration file: + +.. code-block:: python + + 'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + + +Deprecated features +=================== + + +Template tag libraries renamed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following template tag libraries have been renamed: + + * ``pageurl`` => ``wagtailcore_tags`` + * ``rich_text`` => ``wagtailcore_tags`` + * ``embed_filters`` => ``wagtailembeds_tags`` + * ``image_tags`` => ``wagtailimages_tags`` + +The old names will continue to work, but output a ``DeprecationWarning`` - you are advised to update any ``{% load %}`` tags in your templates to refer to the new names. + + +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. + + +``Page.route`` method should now return a ``RouteResult`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the ``route`` method called ``serve`` and returned an ``HttpResponse`` object. This has now been split up so ``serve`` is called separately and ``route`` must now return a RouteResult object. + +If you are overriding ``Page.route`` on any of your page models, you will need to update the method to return a ``RouteResult`` object. The old method of returning an ``HttpResponse`` will continue to work, but this will throw a ``DeprecationWarning`` and bypass the ``before_serve_page`` hook, which means in particular that :ref:`private_pages` will not work on those page types. See :ref:`overriding_route_method`. + + +Wagtailadmins ``hooks`` module has moved to wagtailcore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use any ``wagtail_hooks.py`` files in your project, you may have an import like: ``from wagtail.wagtailadmin import hooks`` + +Change this to: ``from wagtail.wagtailcore import hooks`` + + +Miscellaneous +~~~~~~~~~~~~~ + + * ``Page.show_as_mode`` replaced with ``Page.serve_preview`` + * ``Page.get_page_modes`` method replaced with ``Page.preview_modes`` property + * ``Page.get_other_siblings`` replaced with ``Page.get_siblings(inclusive=False)`` diff --git a/docs/releases/0.5.rst b/docs/releases/0.5.rst new file mode 100644 index 000000000..4b436ddb8 --- /dev/null +++ b/docs/releases/0.5.rst @@ -0,0 +1,27 @@ +========================================== +Wagtail 0.5 release notes - IN DEVELOPMENT +========================================== + +.. contents:: + :local: + :depth: 1 + + +Whats new +========= + + +Minor features +~~~~~~~~~~~~~~ + + +Bug fixes +~~~~~~~~~ + + +Backwards incompatible changes +============================== + + +Deprecated features +=================== diff --git a/docs/releases/index.rst b/docs/releases/index.rst new file mode 100644 index 000000000..9f4e27a51 --- /dev/null +++ b/docs/releases/index.rst @@ -0,0 +1,8 @@ +Release notes +============= + +.. toctree:: + :maxdepth: 1 + + 0.5 + 0.4 diff --git a/docs/search/backends.rst b/docs/search/backends.rst new file mode 100644 index 000000000..f03f06358 --- /dev/null +++ b/docs/search/backends.rst @@ -0,0 +1,66 @@ + +.. _wagtailsearch_backends: + +======== +Backends +======== + + +Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_. + +.. _Elasticsearch: http://www.elasticsearch.org/ + + +.. _wagtailsearch_backends_database: + +Database Backend +================ + +The default DB search backend uses Django's ``__icontains`` filter. + + +Elasticsearch Backend +===================== + +Prerequisites are the Elasticsearch service itself and, via pip, the `elasticsearch-py`_ package: + +.. code-block:: guess + + pip install elasticsearch + +.. note:: + If you are using Elasticsearch < 1.0, install elasticsearch-py version 0.4.5: ```pip install elasticsearch==0.4.5``` + +The backend is configured in settings: + +.. code-block:: python + + WAGTAILSEARCH_BACKENDS = { + 'default': { + 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch', + 'URLS': ['http://localhost:9200'], + 'INDEX': 'wagtail', + 'TIMEOUT': 5, + 'FORCE_NEW': False, + } + } + +Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticsearch-py. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``). + +If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly: + +- Sign up for an account at `dashboard.searchly.com/users/sign\_up`_ +- Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo' +- Note the connection URL from your Searchly dashboard +- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS`` +- Run ``./manage.py update_index`` + +.. _elasticsearch-py: http://elasticsearch-py.readthedocs.org +.. _Searchly: http://www.searchly.com/ +.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up + + +Rolling Your Own +================ + +Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``. \ No newline at end of file diff --git a/docs/search/editors_picks.rst b/docs/search/editors_picks.rst new file mode 100644 index 000000000..9ed41f3c5 --- /dev/null +++ b/docs/search/editors_picks.rst @@ -0,0 +1,41 @@ + +.. _editors-picks: + + +Editors picks +============= + +Editor's Picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content instead of being at the mercy of 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 %} \ No newline at end of file diff --git a/docs/search/for_python_developers.rst b/docs/search/for_python_developers.rst new file mode 100644 index 000000000..1cadbc8da --- /dev/null +++ b/docs/search/for_python_developers.rst @@ -0,0 +1,152 @@ + +.. _wagtailsearch_for_python_developers: + + +===================== +For Python developers +===================== + + +Basic usage +=========== + +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!") + + +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") + [, ] + + +Indexing extra fields +===================== + +Fields need to be explicitly added to the search configuration in order for you to be able to search/filter on them. + +You can add new fields to the search index by overriding the ``search_fields`` property and appending a list of extra ``SearchField``/``FilterField`` objects to it. + +The default value of ``search_fields`` (as set in ``Page``) indexes the ``title`` field as a ``SearchField`` and some other generally useful fields as ``FilterField`` rules. + + +Quick 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`` + + +.. code-block:: python + + from wagtail.wagtailsearch import indexed + + class EventPage(Page): + description = models.TextField() + date = models.DateField() + + search_fields = Page.search_fields + ( # Inherit search_fields from Page + indexed.SearchField('description'), + indexed.FilterField('date'), + ) + + + # Get future events which contain the string "Christmas" in the title or description + >>> EventPage.objects.filter(date__gt=timezone.now()).search("Christmas") + + +``indexed.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. + + +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. + - **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. + + +``indexed.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. + + +Indexing callables and other attributes +--------------------------------------- + + .. note:: + + This is not supported in the :ref:`wagtailsearch_backends_database` + + +Search/filter fields do not need to be Django fields, they could be any method or attribute on your class. + +One use for this is indexing ``get_*_display`` methods Django creates automatically for fields with choices. + + +.. code-block:: python + + from wagtail.wagtailsearch import indexed + + class EventPage(Page): + IS_PRIVATE_CHOICES = ( + (False, "Public"), + (True, "Private"), + ) + + is_private = models.BooleanField(choices=IS_PRIVATE_CHOICES) + + search_fields = Page.search_fields + ( + # Index the human-readable string for searching + indexed.SearchField('get_is_private_display'), + + # Index the boolean value for filtering + indexed.FilterField('is_private'), + ) + + +Indexing non-page models +======================== + +Any Django model can be indexed and searched. + +To do this, inherit from ``indexed.Indexed`` and add some ``search_fields`` to the model. + +.. code-block:: python + + from wagtail.wagtailsearch import indexed + + class Book(models.Model, indexed.Indexed): + title = models.CharField(max_length=255) + genre = models.CharField(max_length=255, choices=GENRE_CHOICES) + author = models.ForeignKey(Author) + published_date = models.DateTimeField() + + search_fields = ( + indexed.SearchField('title', partial_match=True, boost=10), + indexed.SearchField('get_genre_display'), + + indexed.FilterField('genre'), + indexed.FilterField('author'), + indexed.FilterField('published_date'), + ) + + # As this model doesn't have a search method in its QuerySet, we have to call search directly on the backend + >>> from wagtail.wagtailsearch.backends import get_search_backend + >>> s = get_search_backend() + + # Run a search for a book by Roald Dahl + >>> roald_dahl = Author.objects.get(name="Roald Dahl") + >>> s.search("chocolate factory", Book.objects.filter(author=roald_dahl)) + [] diff --git a/docs/wagtail_search.rst b/docs/search/frontend_views.rst similarity index 53% rename from docs/wagtail_search.rst rename to docs/search/frontend_views.rst index 6a77b06f3..1e1ad3fd3 100644 --- a/docs/wagtail_search.rst +++ b/docs/search/frontend_views.rst @@ -1,10 +1,10 @@ -.. _search: +.. _wagtailsearch_frontend_views: -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. +Frontend views +============== + Default Page Search ------------------- @@ -71,43 +71,6 @@ The search view provides a context with a few useful variables. ``query`` A Wagtail ``Query`` object matching the terms. The ``Query`` model provides several class methods for viewing the statistics of all queries, but exposes only one property for single objects, ``query.hits``, which tracks the number of time the search string has been used over the lifetime of the site. ``Query`` also joins to the Editor's Picks functionality though ``query.editors_picks``. See :ref:`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 instead of being at the mercy of 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 %} Asynchronous Search with JSON and AJAX -------------------------------------- @@ -142,30 +105,30 @@ Finally, we'll use JQuery to make the asynchronous requests and handle the inter // when there's something in the input box, make the query searchBox.on('input', function() { if( searchBox.val() == ''){ - resultsBox.html(''); - return; + resultsBox.html(''); + return; } // make the request to the Wagtail JSON search view $.ajax({ - url: wagtailJSONSearchURL + "?q=" + searchBox.val(), - dataType: "json" + url: wagtailJSONSearchURL + "?q=" + searchBox.val(), + dataType: "json" }) .done(function(data) { - console.log(data); - if( data == undefined ){ - resultsBox.html(''); - return; - } - // we're in business! let's format the results - var htmlOutput = ''; - data.forEach(function(element, index, array){ - htmlOutput += '

' + element.title + '

'; - }); - // and display them - resultsBox.html(htmlOutput); + console.log(data); + if( data == undefined ){ + resultsBox.html(''); + return; + } + // we're in business! let's format the results + var htmlOutput = ''; + data.forEach(function(element, index, array){ + htmlOutput += '

' + element.title + '

'; + }); + // and display them + resultsBox.html(htmlOutput); }) .error(function(data){ - console.log(data); + console.log(data); }); }); @@ -178,12 +141,12 @@ Results are returned as a JSON object with this structure: { [ { - title: "Lumpy Space Princess", - url: "/oh-my-glob/" + title: "Lumpy Space Princess", + url: "/oh-my-glob/" }, { - title: "Lumpy Space", - url: "/no-smooth-posers/" + title: "Lumpy Space", + url: "/no-smooth-posers/" }, ... ] @@ -199,67 +162,8 @@ The AJAX interface uses the same view as the normal HTML search, ``wagtailsearch In this template, you'll have access to the same context variables provided to the HTML template. You could provide a template in JSON format with extra properties, such as ``query.hits`` and editor's picks, or render an HTML snippet that can go directly into your results ``
``. If you need more flexibility, such as multiple formats/templates based on differing requests, you can set up a custom search view. -.. _editors-picks: +Custom Search Views +------------------- -Indexing Custom Fields & 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. - - -Search Backends ---------------- - -Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_. - -.. _Elasticsearch: http://www.elasticsearch.org/ - - -Default DB Backend -`````````````````` -The default DB search backend uses Django's ``__icontains`` filter. - - -Elasticsearch Backend -````````````````````` -Prerequisites are the Elasticsearch service itself and, via pip, the `elasticsearch-py`_ package: - -.. code-block:: guess - - pip install elasticsearch - -.. note:: - If you are using Elasticsearch < 1.0, install elasticsearch-py version 0.4.5: ```pip install elasticsearch==0.4.5``` - -The backend is configured in settings: - -.. code-block:: python - - WAGTAILSEARCH_BACKENDS = { - 'default': { - 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch', - 'URLS': ['http://localhost:9200'], - 'INDEX': 'wagtail', - 'TIMEOUT': 5, - 'FORCE_NEW': False, - } - } - -Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticsearch-py. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``). - -If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly: - -- Sign up for an account at `dashboard.searchly.com/users/sign\_up`_ -- Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo' -- Note the connection URL from your Searchly dashboard -- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS`` -- Run ``./manage.py update_index`` - -.. _elasticsearch-py: http://elasticsearch-py.readthedocs.org -.. _Searchly: http://www.searchly.com/ -.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up - -Rolling Your Own -```````````````` -Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``. +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 diff --git a/docs/search/index.rst b/docs/search/index.rst new file mode 100644 index 000000000..e343f76c8 --- /dev/null +++ b/docs/search/index.rst @@ -0,0 +1,17 @@ + +.. _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. + + +.. toctree:: + :maxdepth: 2 + + for_python_developers + frontend_views + editors_picks + backends diff --git a/docs/settings.rst b/docs/settings.rst index 018af5d01..898fb3cb3 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -3,7 +3,7 @@ Configuring Django for Wagtail ============================== -To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see `Writing your first Django app`_. Your project directory will look like the following:: +To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see `Writing your first Django app `_. Your project directory will look like the following:: myproject/ myproject/ @@ -19,13 +19,7 @@ To install Wagtail completely from scratch, create a new Django project and an a views.py manage.py -From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and urlconfs to ``urls.py``. For a more complete view of what's defined in these files, see `Django Settings`_ and `Django URL Dispatcher`_. - -.. _Writing your first Django app: https://docs.djangoproject.com/en/dev/intro/tutorial01/ - -.. _Django Settings: https://docs.djangoproject.com/en/dev/topics/settings/ - -.. _Django URL Dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/ +From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and urlconfs to ``urls.py``. For a more complete view of what's defined in these files, see `Django Settings `__ and `Django URL Dispatcher `_. What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`. @@ -246,6 +240,16 @@ Email Notifications Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. If omitted, Django will fall back to using the ``DEFAULT_FROM_EMAIL`` variable if set, and ``webmaster@localhost`` if not. +Private Pages +------------- + +.. code-block:: python + + PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html' + +This is the path to the Django template which will be used to display the "password required" form when a user accesses a private page. For more details, see the :ref:`private_pages` documentation. + + Other Django Settings Used by Wagtail ------------------------------------- @@ -266,9 +270,7 @@ Other Django Settings Used by Wagtail TEMPLATE_CONTEXT_PROCESSORS USE_I18N -For information on what these settings do, see `Django Settings`_. - -.. _Django Settings: https://docs.djangoproject.com/en/dev/ref/settings/ +For information on what these settings do, see `Django Settings `__. Search Signal Handlers diff --git a/docs/sitemap_generation.rst b/docs/sitemap_generation.rst index 9c6d50e48..203a423fb 100644 --- a/docs/sitemap_generation.rst +++ b/docs/sitemap_generation.rst @@ -1,3 +1,5 @@ +.. _sitemap_generation: + Sitemap generation ================== diff --git a/docs/static_site_generation.rst b/docs/static_site_generation.rst index 9feb59c36..d6b598520 100644 --- a/docs/static_site_generation.rst +++ b/docs/static_site_generation.rst @@ -45,6 +45,8 @@ For example, let's say we have a Blog Index which uses pagination. We can overri .. code:: python + from wagtail.wagtailcore.url_routing import RouteResult + class BlogIndex(Page): ... @@ -54,7 +56,7 @@ For example, let's say we have a Blog Index which uses pagination. We can overri def route(self, request, path_components): if self.live and len(path_components) == 2 and path_components[0] == 'page': try: - return self.serve(request, page=int(path_components[1])) + return RouteResult(self, kwargs={'page': int(path_components[1])}) except (TypeError, ValueError): pass diff --git a/setup.py b/setup.py index 6e0bc6949..129a48468 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,9 @@ PY3 = sys.version_info[0] == 3 install_requires = [ "Django>=1.6.2,<1.7", "South>=0.8.4", - "django-compressor>=1.3", - "django-libsass>=0.1", - "django-modelcluster>=0.1", + "django-compressor>=1.4", + "django-libsass>=0.2", + "django-modelcluster>=0.3", "django-taggit==0.11.2", "django-treebeard==2.0", "Pillow>=2.3.0", @@ -47,7 +47,7 @@ if not PY3: setup( name='wagtail', - version='0.3.1', + version='0.4', description='A Django content management system focused on flexibility and user experience', author='Matthew Westcott', author_email='matthew.westcott@torchbox.com', diff --git a/wagtail/contrib/wagtailfrontendcache/utils.py b/wagtail/contrib/wagtailfrontendcache/utils.py index b95ed7e95..66b77c971 100644 --- a/wagtail/contrib/wagtailfrontendcache/utils.py +++ b/wagtail/contrib/wagtailfrontendcache/utils.py @@ -24,12 +24,17 @@ class CustomHTTPAdapter(HTTPAdapter): return super(CustomHTTPAdapter, self).get_connection(self.cache_url, proxies) -def purge_page_from_cache(page): +def purge_url_from_cache(url): # Get session cache_server_url = getattr(settings, 'WAGTAILFRONTENDCACHE_LOCATION', 'http://127.0.0.1:8000/') session = requests.Session() session.mount('http://', CustomHTTPAdapter(cache_server_url)) - # Purge paths from cache - for path in page.get_cached_paths(): - session.request('PURGE', page.full_url + path[1:]) + # Send purge request to cache + session.request('PURGE', url) + + +def purge_page_from_cache(page): + # Purge cached paths from cache + for path in page.specific.get_cached_paths(): + purge_url_from_cache(page.full_url + path[1:]) diff --git a/wagtail/contrib/wagtailsitemaps/sitemap_generator.py b/wagtail/contrib/wagtailsitemaps/sitemap_generator.py index eb08d81b7..d22f88112 100644 --- a/wagtail/contrib/wagtailsitemaps/sitemap_generator.py +++ b/wagtail/contrib/wagtailsitemaps/sitemap_generator.py @@ -8,7 +8,7 @@ class Sitemap(object): self.site = site def get_pages(self): - return self.site.root_page.get_descendants(inclusive=True).live().order_by('path') + return self.site.root_page.get_descendants(inclusive=True).live().public().order_by('path') def get_urls(self): for page in self.get_pages(): diff --git a/wagtail/contrib/wagtailsitemaps/tests.py b/wagtail/contrib/wagtailsitemaps/tests.py index d2d612fa2..a556ce156 100644 --- a/wagtail/contrib/wagtailsitemaps/tests.py +++ b/wagtail/contrib/wagtailsitemaps/tests.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.core.cache import cache -from wagtail.wagtailcore.models import Page, Site +from wagtail.wagtailcore.models import Page, PageViewRestriction, Site from wagtail.tests.models import SimplePage from .sitemap_generator import Sitemap @@ -23,6 +23,13 @@ class TestSitemapGenerator(TestCase): live=False, )) + self.protected_child_page = self.home_page.add_child(instance=SimplePage( + title="Protected", + slug='protected', + live=True, + )) + PageViewRestriction.objects.create(page=self.protected_child_page, password='hello') + self.site = Site.objects.get(is_default_site=True) def test_get_pages(self): @@ -31,23 +38,27 @@ class TestSitemapGenerator(TestCase): self.assertIn(self.child_page.page_ptr, pages) self.assertNotIn(self.unpublished_child_page.page_ptr, pages) + self.assertNotIn(self.protected_child_page.page_ptr, pages) def test_get_urls(self): sitemap = Sitemap(self.site) urls = [url['location'] for url in sitemap.get_urls()] - self.assertIn('/', urls) # Homepage - self.assertIn('/hello-world/', urls) # Child page + self.assertIn('http://localhost/', urls) # Homepage + self.assertIn('http://localhost/hello-world/', urls) # Child page def test_render(self): sitemap = Sitemap(self.site) xml = sitemap.render() # Check that a URL has made it into the xml - self.assertIn('/hello-world/', xml) + self.assertIn('http://localhost/hello-world/', xml) # Make sure the unpublished page didn't make it into the xml - self.assertNotIn('/unpublished/', xml) + self.assertNotIn('http://localhost/unpublished/', xml) + + # Make sure the protected page didn't make it into the xml + self.assertNotIn('http://localhost/protected/', xml) class TestSitemapView(TestCase): diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 467786b2d..f31610279 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -23,7 +23,7 @@ "model": "wagtailcore.page", "fields": { "title": "Welcome to the Wagtail test site!", - "numchild": 3, + "numchild": 5, "show_in_menus": false, "live": true, "depth": 2, @@ -257,6 +257,79 @@ } }, +{ + "pk": 10, + "model": "wagtailcore.page", + "fields": { + "title": "Old style route method", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 3, + "content_type": ["tests", "pagewitholdstyleroutemethod"], + "path": "000100010004", + "url_path": "/home/old-style-route/", + "slug": "old-style-route" + } +}, +{ + "pk": 10, + "model": "tests.pagewitholdstyleroutemethod", + "fields": { + "content": "

Test with old style route method

" + } +}, + +{ + "pk": 11, + "model": "wagtailcore.page", + "fields": { + "title": "Secret plans", + "numchild": 1, + "show_in_menus": true, + "live": true, + "depth": 3, + "content_type": ["tests", "simplepage"], + "path": "000100010005", + "url_path": "/home/secret-plans/", + "slug": "secret-plans" + } +}, +{ + "pk": 11, + "model": "tests.simplepage", + "fields": { + "content": "

muahahahaha

" + } +}, + +{ + "pk": 12, + "model": "wagtailcore.page", + "fields": { + "title": "Steal underpants", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000100050001", + "url_path": "/home/secret-plans/steal-underpants/", + "slug": "steal-underpants" + } +}, +{ + "pk": 12, + "model": "tests.eventpage", + "fields": { + "date_from": "2015-07-04", + "audience": "private", + "location": "Marks and Spencer", + "body": "

meet in the menswear department at noon

", + "cost": "free" + } +}, + { "pk": 1, "model": "wagtailcore.site", @@ -489,6 +562,14 @@ "submit_time": "2014-01-01T12:00:00.000Z" } }, +{ + "pk": 1, + "model": "tests.advert", + "fields": { + "text": "test_advert", + "url": "http://www.example.com" + } +}, { "pk": 1, "model": "wagtaildocs.Document", @@ -506,5 +587,14 @@ "width": 0, "height": 0 } +}, + +{ + "pk": 1, + "model": "wagtailcore.pageviewrestriction", + "fields": { + "page": 11, + "password": "swordfish" + } } ] diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index ff6dc2dcc..2f151c465 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -11,6 +11,7 @@ from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField from wagtail.wagtailsnippets.models import register_snippet +from wagtail.wagtailsearch import indexed EVENT_AUDIENCE_CHOICES = ( @@ -106,6 +107,19 @@ class SimplePage(Page): content = models.TextField() +class PageWithOldStyleRouteMethod(Page): + """ + Prior to Wagtail 0.4, the route() method on Page returned an HttpResponse + rather than a Page instance. As subclasses of Page may override route, + we need to continue accepting this convention (albeit as a deprecated API). + """ + content = models.TextField() + template = 'tests/simple_page.html' + + def route(self, request, path_components): + return self.serve(request) + + # Event page class EventPageCarouselItem(Orderable, CarouselItem): @@ -166,6 +180,8 @@ class EventPage(Page): indexed_fields = ('get_audience_display', 'location', 'body') search_name = "Event" + password_required_template = 'tests/event_page_password_required.html' + EventPage.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('date_from'), @@ -316,3 +332,60 @@ class BusinessSubIndex(Page): class BusinessChild(Page): subpage_types = [] + + +class SearchTest(models.Model, indexed.Indexed): + title = models.CharField(max_length=255) + content = models.TextField() + live = models.BooleanField(default=False) + published_date = models.DateField(null=True) + + search_fields = [ + indexed.SearchField('title', partial_match=True), + indexed.SearchField('content'), + indexed.SearchField('callable_indexed_field'), + indexed.FilterField('title'), + indexed.FilterField('live'), + indexed.FilterField('published_date'), + ] + + def callable_indexed_field(self): + return "Callable" + + +class SearchTestChild(SearchTest): + subtitle = models.CharField(max_length=255, null=True, blank=True) + extra_content = models.TextField() + + search_fields = SearchTest.search_fields + [ + indexed.SearchField('subtitle', partial_match=True), + indexed.SearchField('extra_content'), + ] + + +class SearchTestOldConfig(models.Model, indexed.Indexed): + """ + This tests that the Indexed class can correctly handle models that + use the old "indexed_fields" configuration format. + """ + indexed_fields = { + # A search field with predictive search and boosting + 'title': { + 'type': 'string', + 'analyzer': 'edgengram_analyzer', + 'boost': 100, + }, + + # A filter field + 'live': { + 'type': 'boolean', + 'index': 'not_analyzed', + }, + } + +class SearchTestOldConfigList(models.Model, indexed.Indexed): + """ + This tests that the Indexed class can correctly handle models that + use the old "indexed_fields" configuration format using a list. + """ + indexed_fields = ['title', 'content'] diff --git a/wagtail/tests/templates/tests/event_page_password_required.html b/wagtail/tests/templates/tests/event_page_password_required.html new file mode 100644 index 000000000..e58d740ad --- /dev/null +++ b/wagtail/tests/templates/tests/event_page_password_required.html @@ -0,0 +1,15 @@ + + + + {{ self.title }} + + +

{{ self.title }}

+

This event is invitation only. Please enter your password to see the details.

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/wagtail/tests/urls.py b/wagtail/tests/urls.py index 83e12adfb..c9ca98bba 100644 --- a/wagtail/tests/urls.py +++ b/wagtail/tests/urls.py @@ -3,17 +3,13 @@ from django.conf.urls import patterns, include, url from wagtail.wagtailcore import urls as wagtail_urls from wagtail.wagtailadmin import urls as wagtailadmin_urls from wagtail.wagtaildocs import urls as wagtaildocs_urls -from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls +from wagtail.wagtailsearch import urls as wagtailsearch_urls from wagtail.contrib.wagtailsitemaps.views import sitemap -# Signal handlers -from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers -wagtailsearch_register_signal_handlers() - urlpatterns = patterns('', url(r'^admin/', include(wagtailadmin_urls)), - url(r'^search/', include(wagtailsearch_frontend_urls)), + url(r'^search/', include(wagtailsearch_urls)), url(r'^documents/', include(wagtaildocs_urls)), url(r'^sitemap\.xml$', sitemap), diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index c44549401..2e95d0210 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,8 @@ +from contextlib import contextmanager +import warnings + from django.contrib.auth.models import User +from django.utils import six # We need to make sure that we're using the same unittest library that Django uses internally # Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors @@ -21,3 +25,17 @@ class WagtailTestUtils(object): self.client.login(username='test', password='password') return user + + def assertRegex(self, *args, **kwargs): + six.assertRegex(self, *args, **kwargs) + + @staticmethod + @contextmanager + def ignore_deprecation_warnings(): + with warnings.catch_warnings(record=True) as warning_list: # catch all warnings + yield + + # rethrow all warnings that were not DeprecationWarnings + for w in warning_list: + if not issubclass(w.category, DeprecationWarning): + warnings.showwarning(message=w.message, category=w.category, filename=w.filename, lineno=w.lineno, file=w.file, line=w.line) diff --git a/wagtail/tests/wagtail_hooks.py b/wagtail/tests/wagtail_hooks.py index 13e3e46d2..8c398ed2c 100644 --- a/wagtail/tests/wagtail_hooks.py +++ b/wagtail/tests/wagtail_hooks.py @@ -1,3 +1,5 @@ +from django.http import HttpResponse + from wagtail.wagtailcore import hooks from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes @@ -17,3 +19,9 @@ def whitelister_element_rules(): 'a': attribute_rule({'href': check_url, 'target': True}), } hooks.register('construct_whitelister_element_rules', whitelister_element_rules) + + +def block_googlebot(page, request, serve_args, serve_kwargs): + if request.META.get('HTTP_USER_AGENT') == 'GoogleBot': + return HttpResponse("

bad googlebot no cookie

") +hooks.register('before_serve_page', block_googlebot) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index ba6edc4fa..d2eb181c3 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -575,6 +575,11 @@ class BaseInlinePanel(EditHandler): child_edit_handler_class(instance=subform.instance, form=subform) ) + # if this formset is valid, it may have been re-ordered; respect that + # in case the parent form errored and we need to re-render + if self.formset.can_order and self.formset.is_valid(): + self.children = sorted(self.children, key=lambda x: x.form.cleaned_data['ORDER']) + empty_form = self.formset.empty_form empty_form.fields['DELETE'].widget = forms.HiddenInput() if self.formset.can_order: diff --git a/wagtail/wagtailadmin/forms.py b/wagtail/wagtailadmin/forms.py index a8da2525e..51854e605 100644 --- a/wagtail/wagtailadmin/forms.py +++ b/wagtail/wagtailadmin/forms.py @@ -83,3 +83,20 @@ class CopyForm(forms.Form): new_slug = forms.CharField() copy_subpages = forms.BooleanField(required=False) publish_copies = forms.BooleanField(required=False) + + +class PageViewRestrictionForm(forms.Form): + restriction_type = forms.ChoiceField(label="Visibility", choices=[ + ('none', ugettext_lazy("Public")), + ('password', ugettext_lazy("Private, accessible with the following password")), + ], widget=forms.RadioSelect) + password = forms.CharField(required=False) + + def clean(self): + cleaned_data = super(PageViewRestrictionForm, self).clean() + + if cleaned_data.get('restriction_type') == 'password' and not cleaned_data.get('password'): + self._errors["password"] = self.error_class([_('This field is required.')]) + del cleaned_data['password'] + + return cleaned_data diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/privacy-indicator.js b/wagtail/wagtailadmin/static/wagtailadmin/js/privacy-indicator.js new file mode 100644 index 000000000..eb280f00e --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/privacy-indicator.js @@ -0,0 +1,18 @@ +$(function() { + /* Interface to set permissions from the explorer / editor */ + $('a.action-set-privacy').click(function() { + ModalWorkflow({ + 'url': this.href, + 'responses': { + 'setPermission': function(isPublic) { + if (isPublic) { + $('.privacy-indicator').removeClass('private').addClass('public'); + } else { + $('.privacy-indicator').removeClass('public').addClass('private'); + } + } + } + }); + return false; + }); +}); diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss index 97e915f36..b6f5ca03c 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/formatters.scss @@ -121,8 +121,8 @@ color:lighten($color-grey-2,30%); -webkit-font-smoothing: auto; font-size:0.80em; - margin:0 0.5em; - background:white url("#{$static-root}bg-dark-diag.svg"); + margin:0 0.5em 0.5em; + background:white url( "#{$static-root}bg-dark-diag.svg"); &.primary{ color:$color-grey-2; @@ -136,6 +136,26 @@ a.status-tag.primary:hover{ color:$color-teal; } +.privacy-indicator { + .label-private, .label-public{ + &:before{ + font-size:1.5em; + color:$color-teal; + } + } + &.public { + .label-private { + display: none; + } + } + &.private { + .label-public { + display: none; + } + } +} + + /* free tagging tags from taggit */ .tag{ background-color:$color-teal; @@ -174,6 +194,7 @@ a.tag:hover{ } } + /* make a block-level element inline */ .inline{ display:inline; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss index 3a8f67ba2..d71ff66da 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss @@ -69,6 +69,10 @@ input, textarea, select, .richtext, .tagit{ outline:none; background-color:$color-input-focus; } + &:disabled, &[disabled], &:disabled:hover, &[disabled]:hover{ + background-color:inherit; + cursor:not-allowed; + } } /* select boxes */ @@ -135,6 +139,7 @@ input[type=radio]:before{ display:block; content:"K"; width: 1em; + height:1em; line-height: 1.1em; padding: 4px; background-color: white; @@ -741,6 +746,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{ .choice_field &, .model_multiple_choice_field &, .boolean_field &, + .choice_field &, .model_choice_field &, .image_field &, .file_field &{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss index 3f079897f..1e9905d27 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss @@ -5,6 +5,10 @@ header{ margin-bottom:2em; color:white; + a{ + color:white; + } + h1, h2{ margin:0; color:white; @@ -97,12 +101,6 @@ header{ } } -.page-explorer header{ - margin-bottom:0; - padding-bottom:0em; -} - - @media screen and (min-width: $breakpoint-mobile){ header{ padding-top:1.5em; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/listing.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/listing.scss index 9d940362a..537465083 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/listing.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/listing.scss @@ -343,7 +343,6 @@ ul.listing{ background:$color-teal-darker; } } - } } ul.listing{ @@ -357,6 +356,8 @@ table.listing{ /* explorer specific tweaks */ .page-explorer .listing { + position:relative; + .index{ color:white; background-color:$color-header-bg; @@ -366,6 +367,14 @@ table.listing{ padding-bottom:1.5em; } + .privacy-indicator{ + font-size:1em; + opacity:1; + position:absolute; + right:5%; + top:2em; + } + .title{ h2{ color:white; @@ -376,9 +385,15 @@ table.listing{ color:white; } } + } } + .privacy-indicator{ + font-size:0.9em; + opacity:0.7; + } + .table-headers{ .ord{ padding-right:0; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/normalize.css b/wagtail/wagtailadmin/static/wagtailadmin/scss/normalize.css index 8d57e3c96..8d945b3ec 100755 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/normalize.css +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/normalize.css @@ -452,15 +452,6 @@ input[type="submit"] { *overflow: visible; /* 4 */ } -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - /** * 1. Address box sizing set to content-box in IE 8/9. * 2. Remove excess padding in IE 8/9. diff --git a/wagtail/wagtailadmin/taggable.py b/wagtail/wagtailadmin/taggable.py index 39213e956..3b7de479c 100644 --- a/wagtail/wagtailadmin/taggable.py +++ b/wagtail/wagtailadmin/taggable.py @@ -4,31 +4,28 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Count from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from wagtail.wagtailsearch import Indexed, get_search_backend +from wagtail.wagtailsearch import indexed +from wagtail.wagtailsearch.backends import get_search_backend -class TagSearchable(Indexed): +class TagSearchable(indexed.Indexed): """ Mixin to provide a 'search' method, searching on the 'title' field and tags, for models that provide those things. """ - indexed_fields = { - 'title': { - 'type': 'string', - 'analyzer': 'edgengram_analyzer', - 'boost': 10, - }, - 'get_tags': { - 'type': 'string', - 'analyzer': 'edgengram_analyzer', - 'boost': 10, - }, - } + search_fields = ( + indexed.SearchField('title', partial_match=True, boost=10), + indexed.SearchField('get_tags', partial_match=True, boost=10) + ) @property def get_tags(self): - return ' '.join([tag.name for tag in self.tags.all()]) + return ' '.join([tag.name for tag in self.prefetched_tags()]) + + @classmethod + def get_indexed_objects(cls): + return super(TagSearchable, cls).get_indexed_objects().prefetch_related('tagged_items__tag') @classmethod def search(cls, q, results_per_page=None, page=1, prefetch_tags=False, filters={}): diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/ancestor_privacy.html b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/ancestor_privacy.html new file mode 100644 index 000000000..b1471ba25 --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/ancestor_privacy.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% trans "Page privacy" as title_str %} +{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %} + +
+

{% trans "This page has been made private by a parent page." %}

+

{% trans "You can edit the privacy settings on:" %} {{ page_with_restriction.title }} +

diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.html b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.html new file mode 100644 index 000000000..6298d6f5f --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% trans "Page privacy" as title_str %} +{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %} + +
+

{% trans "Note: privacy changes apply to all children of this page too." %}

+
+ {% csrf_token %} +
    + {% include "wagtailadmin/shared/field_as_li.html" with field=form.restriction_type %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.password li_classes="password-field" %} +
+ +
+ +
diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.js b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.js new file mode 100644 index 000000000..0a40c0f85 --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.js @@ -0,0 +1,19 @@ +function(modal) { + $('form', modal.body).submit(function() { + modal.postForm(this.action, $(this).serialize()); + return false; + }); + + var restrictionTypePasswordField = $("input[name='restriction_type'][value='password']", modal.body); + var passwordField = $("#id_password", modal.body); + function refreshFormFields() { + if (restrictionTypePasswordField.is(':checked')) { + passwordField.removeAttr('disabled'); + } else { + passwordField.attr('disabled', true); + } + } + refreshFormFields(); + + $("input[name='restriction_type']", modal.body).change(refreshFormFields); +} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy_done.js b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy_done.js new file mode 100644 index 000000000..dafbc7b36 --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy_done.js @@ -0,0 +1,4 @@ +function(modal) { + modal.respond('setPermission', {% if is_public %}true{% else %}false{% endif %}); + modal.close(); +} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html index 17bcbd8bf..a3185e096 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html @@ -21,6 +21,7 @@ + {% hook_output 'insert_editor_js' %} {% endcompress %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_privacy_indicator.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_privacy_indicator.html new file mode 100644 index 000000000..53aa44800 --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_privacy_indicator.html @@ -0,0 +1,23 @@ +{% load i18n wagtailadmin_tags %} + +{% test_page_is_public page as is_public %} +{% if not page_perms %} + {% page_permissions page as page_perms %} +{% endif %} + +
+ {% trans "Privacy" %} + {% if page_perms.can_set_view_restrictions %} + + {# labels are shown/hidden in CSS according to the 'private' / 'public' class on view-permission-indicator #} + {% trans 'Public' %} + {% trans 'Private' %} + + {% else %} + {% if is_public %} + {% trans 'Public' %} + {% else %} + {% trans 'Private' %} + {% endif %} + {% endif %} +
diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html index b87af8fae..f21b93a27 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html @@ -39,12 +39,12 @@
  • {% trans 'Preview' as preview_label %} - {% if display_modes|length > 1 %} + {% if preview_modes|length > 1 %}
    - {% trans "Status:" %} {% if page.live %}{{ page.status_string }}{% else %}{{ page.status_string }}{% endif %} + {% trans "Status" %} + {% if page.live %} + {{ page.status_string }} + {% else %} + {{ page.status_string }} + {% endif %} + + {% include "wagtailadmin/pages/_privacy_indicator.html" with page=page page_perms=page_perms only %}
  • @@ -22,8 +30,7 @@
    {% csrf_token %} {{ edit_handler.render_form_content }} - - {% page_permissions page as page_perms %} +