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 c24040364..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. diff --git a/docs/conf.py b/docs/conf.py index 11517ada2..ba6525d1b 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.3.1' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.3.1' # 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 a12d883d3..48d766675 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -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`` diff --git a/docs/index.rst b/docs/index.rst index d200f2bc1..171c0e45a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,7 @@ It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 suppo advanced_topics deploying performance + private_pages static_site_generation frontend_cache_purging sitemap_generation diff --git a/docs/management_commands.rst b/docs/management_commands.rst index 96cfe95c3..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 ---------- @@ -30,6 +41,9 @@ Options: - **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 e799ef70a..7f9b5f7b6 100644 --- a/docs/model_recipes.rst +++ b/docs/model_recipes.rst @@ -58,8 +58,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 +67,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 +85,7 @@ First, ``models.py``: .. code-block:: python from django.shortcuts import render + from wagtail.wagtailcore.url_routing import RouteResult ... @@ -89,15 +93,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 +116,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 +126,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: 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..bfd20ee09 --- /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..a51c3402d --- /dev/null +++ b/docs/releases/0.4.rst @@ -0,0 +1,175 @@ +========================= +Wagtail 0.4 release notes +========================= + + +Whats 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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Its now possible to perform searches with Elasticsearch on ``PageQuerySet``s: + + >>> 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 recieve 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 ``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 + + +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 (mainly for wagtail developers) + + +Bug fixes +~~~~~~~~~ + + * Animated GIFs are now coalesced before resizing + * Wand backend clones images before modifying them + * Admin breadcrumb now positioned correctly on mobile + * 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 + * 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. + + +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`` + + +New search field configuration format +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``indexed_fields`` is now deprecated and has been replaced by a new search field configuration format called ``search_fields``. + + +``Page.route`` method should now return a ``RouteResult`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the ``route`` method called ``serve`` and returned a ``HttpResponse`` object. This has now been split up so ``serve`` is called separately and ``route`` must now return a RouteResult object. + +:ref:`anatomy_of_a_wagtail_request` + + +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/settings.rst b/docs/settings.rst index 66fc2de98..898fb3cb3 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -240,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 ------------------------------------- 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/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..469e210ad 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,6 +38,7 @@ 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) @@ -49,6 +57,9 @@ class TestSitemapGenerator(TestCase): # Make sure the unpublished page didn't make it into the xml self.assertNotIn('/unpublished/', xml) + # Make sure the protected page didn't make it into the xml + self.assertNotIn('/protected/', xml) + class TestSitemapView(TestCase): def test_sitemap_view(self): diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 2d3db6e35..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", @@ -514,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..81f30f82b 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -106,6 +106,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 +179,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'), 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..9360f2206 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,5 @@ 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 +22,6 @@ class WagtailTestUtils(object): self.client.login(username='test', password='password') return user + + def assertRegex(self, *args, **kwargs): + six.assertRegex(self, *args, **kwargs) 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 e55a831f4..e9b10f63c 100644 --- a/wagtail/wagtailadmin/forms.py +++ b/wagtail/wagtailadmin/forms.py @@ -75,3 +75,20 @@ class PasswordResetForm(PasswordResetForm): raise forms.ValidationError(_("This email address is not recognised.")) return cleaned_data + + +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/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/edit.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/edit.html index 8cc7fdca6..c15efbd59 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/edit.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/edit.html @@ -6,6 +6,7 @@ {% block bodyclass %}menu-explorer page-editor{% endblock %} {% block content %} + {% page_permissions page as page_perms %}
{% include "wagtailadmin/shared/breadcrumb.html" with page=page %} @@ -14,7 +15,14 @@

{% blocktrans with title=page.title %}Editing {{ title }}{% endblocktrans %}

- {% 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 %} +