mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-28 08:28:18 +00:00
Merge pull request #426 from torchbox/feature/fep2
Front-end permissions
This commit is contained in:
commit
c0eeb81cd3
45 changed files with 1273 additions and 70 deletions
|
|
@ -177,9 +177,10 @@ 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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("<h1>bad googlebot no cookie</h1>")
|
||||
|
||||
hooks.register('before_serve_page', block_googlebot)
|
||||
|
||||
.. _construct_wagtail_edit_bird:
|
||||
|
||||
``construct_wagtail_edit_bird``
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
64
docs/private_pages.rst
Normal file
64
docs/private_pages.rst
Normal file
|
|
@ -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
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Password required</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Password required</h1>
|
||||
<p>You need a password to access this page.</p>
|
||||
<form action="{{ action_url }}" method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form.non_field_errors }}
|
||||
|
||||
<div>
|
||||
{{ form.password.errors }}
|
||||
{{ form.password.label_tag }}
|
||||
{{ form.password }}
|
||||
</div>
|
||||
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<input type="submit" value="Continue" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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'
|
||||
|
|
@ -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
|
||||
-------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
84
wagtail/tests/fixtures/test.json
vendored
84
wagtail/tests/fixtures/test.json
vendored
|
|
@ -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": "<p>Test with old style route method</p>"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"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": "<p>muahahahaha</p>"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"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": "<p>meet in the menswear department at noon</p>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ self.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ self.title }}</h1>
|
||||
<p>This event is invitation only. Please enter your password to see the details.</p>
|
||||
<form action="{{ action_url }}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Continue" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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("<h1>bad googlebot no cookie</h1>")
|
||||
hooks.register('before_serve_page', block_googlebot)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 &{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
{% load i18n %}
|
||||
{% trans "Page privacy" as title_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<p>{% trans "This page has been made private by a parent page." %}</p>
|
||||
<p>{% trans "You can edit the privacy settings on:" %} <a href="{% url 'wagtailadmin_pages_edit' page_with_restriction.id %}">{{ page_with_restriction.title }}</a>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{% load i18n %}
|
||||
{% trans "Page privacy" as title_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<p>{% trans "<b>Note:</b> privacy changes apply to all children of this page too." %}</p>
|
||||
<form action="{% url 'wagtailadmin_pages_set_privacy' page.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<ul class="fields">
|
||||
{% 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" %}
|
||||
</ul>
|
||||
<input type="submit" value="Save" />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
function(modal) {
|
||||
modal.respond('setPermission', {% if is_public %}true{% else %}false{% endif %});
|
||||
modal.close();
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
<script src="{{ STATIC_URL }}wagtailadmin/js/page-editor.js"></script>
|
||||
<script src="{{ STATIC_URL }}wagtailadmin/js/page-chooser.js"></script>
|
||||
<script src="{{ STATIC_URL }}admin/js/urlify.js"></script>
|
||||
<script src="{{ STATIC_URL }}wagtailadmin/js/privacy-indicator.js"></script>
|
||||
|
||||
{% hook_output 'insert_editor_js' %}
|
||||
{% endcompress %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
<div class="privacy-indicator {% if is_public %}public{% else %}private{% endif %}">
|
||||
{% trans "Privacy" %}
|
||||
{% if page_perms.can_set_view_restrictions %}
|
||||
<a href="{% url 'wagtailadmin_pages_set_privacy' page.id %}" class="status-tag primary action-set-privacy">
|
||||
{# labels are shown/hidden in CSS according to the 'private' / 'public' class on view-permission-indicator #}
|
||||
<span class="label-public icon icon-unlocked">{% trans 'Public' %}</span>
|
||||
<span class="label-private icon icon-locked">{% trans 'Private' %}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{% if is_public %}
|
||||
<span class="label-public status-tag primary icon icon-unlocked ">{% trans 'Public' %}</span>
|
||||
{% else %}
|
||||
<span class="label-private status-tag primary icon icon-locked">{% trans 'Private' %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
{% block bodyclass %}menu-explorer page-editor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% page_permissions page as page_perms %}
|
||||
<header class="merged tab-merged nice-padding">
|
||||
{% include "wagtailadmin/shared/breadcrumb.html" with page=page %}
|
||||
|
||||
|
|
@ -14,7 +15,14 @@
|
|||
<h1 class="icon icon-doc-empty-inverse">{% blocktrans with title=page.title %}Editing <span>{{ title }}</span>{% endblocktrans %}</h1>
|
||||
</div>
|
||||
<div class="right col3">
|
||||
{% trans "Status:" %} {% if page.live %}<a href="{{ page.url }}" class="status-tag {% if page.live %}primary{% endif %}">{{ page.status_string }}</a>{% else %}<span class="status-tag">{{ page.status_string }}</span>{% endif %}
|
||||
{% trans "Status" %}
|
||||
{% if page.live %}
|
||||
<a href="{{ page.url }}" class="status-tag {% if page.live %}primary{% endif %}">{{ page.status_string }}</a>
|
||||
{% else %}
|
||||
<span class="status-tag">{{ page.status_string }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% include "wagtailadmin/pages/_privacy_indicator.html" with page=page page_perms=page_perms only %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -22,8 +30,7 @@
|
|||
<form id="page-edit-form" action="{% url 'wagtailadmin_pages_edit' page.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ edit_handler.render_form_content }}
|
||||
|
||||
{% page_permissions page as page_perms %}
|
||||
|
||||
<footer>
|
||||
<ul>
|
||||
<li class="actions">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load wagtailadmin_tags %}
|
||||
{% load i18n wagtailadmin_tags compress %}
|
||||
{% block titletag %}{% blocktrans with title=parent_page.title %}Exploring {{ title }}{% endblocktrans %}{% endblock %}
|
||||
{% block bodyclass %}menu-explorer page-explorer {% if ordering == 'ord' %}reordering{% endif %}{% endblock %}
|
||||
|
||||
|
|
@ -17,10 +16,13 @@
|
|||
{% page_permissions parent_page as parent_page_perms %}
|
||||
{% include "wagtailadmin/pages/list.html" with sortable=1 allow_navigation=1 full_width=1 parent_page=parent_page orderable=parent_page_perms.can_reorder_children %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% comment %} modal-workflow is required by the view restrictions interface {% endcomment %}
|
||||
<script src="{{ STATIC_URL }}wagtailadmin/js/modal-workflow.js"></script>
|
||||
<script src="{{ STATIC_URL }}wagtailadmin/js/privacy-indicator.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
{% if ordering == 'ord' %}
|
||||
$(function() {
|
||||
|
|
|
|||
|
|
@ -31,23 +31,42 @@
|
|||
<tr class="index {% if not parent_page.live %} inactive{% endif %} {% if moving or choosing %}{% if parent_page.can_choose %}can-choose{% endif %}{% endif %}">
|
||||
<td class="title" {% if orderable %}colspan="2"{% endif %}>
|
||||
{% if moving %}
|
||||
{% if parent_page.can_choose %}
|
||||
<h2><a href="{% url 'wagtailadmin_pages_move_confirm' page_to_move.id parent_page.id %}">{{ parent_page.title }}</a></h2>
|
||||
{% else %}
|
||||
<h2>{{ parent_page.title }}</h2>
|
||||
{% endif %}
|
||||
<h2>
|
||||
{% if parent_page.can_choose %}
|
||||
<a href="{% url 'wagtailadmin_pages_move_confirm' page_to_move.id parent_page.id %}">{{ parent_page.title }}</a>
|
||||
{% else %}
|
||||
{{ parent_page.title }}
|
||||
{% endif %}
|
||||
|
||||
{% test_page_is_public parent_page as is_public %}
|
||||
{% if not is_public %}
|
||||
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% elif choosing %}
|
||||
{% if parent_page.can_choose %}
|
||||
<h2><a class="choose-page" href="#{{ parent_page.id }}" data-id="{{ parent_page.id }}" data-title="{{ parent_page.title }}" data-url="{{ parent_page.url }}">{{ parent_page.title }}</a></h2>
|
||||
{% else %}
|
||||
<h2>{{ parent_page.title }}</h2>
|
||||
{% endif %}
|
||||
<h2>
|
||||
{% if parent_page.can_choose %}
|
||||
<a class="choose-page" href="#{{ parent_page.id }}" data-id="{{ parent_page.id }}" data-title="{{ parent_page.title }}" data-url="{{ parent_page.url }}">{{ parent_page.title }}</a>
|
||||
{% else %}
|
||||
{{ parent_page.title }}
|
||||
{% endif %}
|
||||
|
||||
{% test_page_is_public parent_page as is_public %}
|
||||
{% if not is_public %}
|
||||
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% else %}
|
||||
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
|
||||
<h2><a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}">{{ parent_page.title }}</a></h2>
|
||||
{% else %}
|
||||
<h2>{{ parent_page.title }}</h2>
|
||||
{% endif %}
|
||||
<h2>
|
||||
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
|
||||
<a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}">{{ parent_page.title }}</a>
|
||||
{% else %}
|
||||
{{ parent_page.title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% include "wagtailadmin/pages/_privacy_indicator.html" with page=parent_page %}
|
||||
|
||||
<ul class="actions">
|
||||
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
|
||||
<li><a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}" class="button button-small">{% trans 'Edit' %}</a></li>
|
||||
|
|
@ -159,6 +178,11 @@
|
|||
{{ page.title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% test_page_is_public page as is_public %}
|
||||
{% if not is_public %}
|
||||
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if not moving and not choosing %}
|
||||
<ul class="actions">
|
||||
|
|
@ -249,4 +273,4 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy
|
||||
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy, PageViewRestriction
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore
|
||||
|
||||
|
||||
|
|
@ -61,7 +61,10 @@ def fieldtype(bound_field):
|
|||
try:
|
||||
return camelcase_to_underscore(bound_field.field.__class__.__name__)
|
||||
except AttributeError:
|
||||
return ""
|
||||
try:
|
||||
return camelcase_to_underscore(bound_field.__class__.__name__)
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
@ -88,6 +91,26 @@ def page_permissions(context, page):
|
|||
return context['user_page_permissions'].for_page(page)
|
||||
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def test_page_is_public(context, page):
|
||||
"""
|
||||
Usage: {% test_page_is_public page as is_public %}
|
||||
Sets 'is_public' to True iff there are no page view restrictions in place on
|
||||
this page.
|
||||
Caches the list of page view restrictions in the context, to avoid repeated
|
||||
DB queries on repeated calls.
|
||||
"""
|
||||
if 'all_page_view_restriction_paths' not in context:
|
||||
context['all_page_view_restriction_paths'] = PageViewRestriction.objects.select_related('page').values_list('page__path', flat=True)
|
||||
|
||||
is_private = any([
|
||||
page.path.startswith(restricted_path)
|
||||
for restricted_path in context['all_page_view_restriction_paths']
|
||||
])
|
||||
|
||||
return not is_private
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def hook_output(hook_name):
|
||||
"""
|
||||
|
|
|
|||
261
wagtail/wagtailadmin/tests/test_privacy.py
Normal file
261
wagtail/wagtailadmin/tests/test_privacy.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailcore.models import Page, PageViewRestriction
|
||||
from wagtail.tests.models import SimplePage
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
||||
class TestSetPrivacyView(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
# Create some pages
|
||||
self.homepage = Page.objects.get(id=2)
|
||||
|
||||
self.public_page = self.homepage.add_child(instance=SimplePage(
|
||||
title="Public page",
|
||||
slug='public-page',
|
||||
live=True,
|
||||
))
|
||||
|
||||
self.private_page = self.homepage.add_child(instance=SimplePage(
|
||||
title="Private page",
|
||||
slug='private-page',
|
||||
live=True,
|
||||
))
|
||||
PageViewRestriction.objects.create(page=self.private_page, password='password123')
|
||||
|
||||
self.private_child_page = self.private_page.add_child(instance=SimplePage(
|
||||
title="Private child page",
|
||||
slug='private-child-page',
|
||||
live=True,
|
||||
))
|
||||
|
||||
def test_get_public(self):
|
||||
"""
|
||||
This tests that a blank form is returned when a user opens the set_privacy view on a public page
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_set_privacy', args=(self.public_page.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/page_privacy/set_privacy.html')
|
||||
self.assertEqual(response.context['page'].specific, self.public_page)
|
||||
|
||||
# Check form attributes
|
||||
self.assertEqual(response.context['form']['restriction_type'].value(), 'none')
|
||||
|
||||
def test_get_private(self):
|
||||
"""
|
||||
This tests that the restriction type and password fields as set correctly when a user opens the set_privacy view on a public page
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_set_privacy', args=(self.private_page.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/page_privacy/set_privacy.html')
|
||||
self.assertEqual(response.context['page'].specific, self.private_page)
|
||||
|
||||
# Check form attributes
|
||||
self.assertEqual(response.context['form']['restriction_type'].value(), 'password')
|
||||
self.assertEqual(response.context['form']['password'].value(), 'password123')
|
||||
|
||||
def test_get_private_child(self):
|
||||
"""
|
||||
This tests that the set_privacy view tells the user that the password restriction has been applied to an ancestor
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_set_privacy', args=(self.private_child_page.id, )))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/page_privacy/ancestor_privacy.html')
|
||||
self.assertEqual(response.context['page_with_restriction'].specific, self.private_page)
|
||||
|
||||
def test_set_password_restriction(self):
|
||||
"""
|
||||
This tests that setting a password restriction using the set_privacy view works
|
||||
"""
|
||||
post_data = {
|
||||
'restriction_type': 'password',
|
||||
'password': 'helloworld',
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_set_privacy', args=(self.public_page.id, )), post_data)
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "modal.respond('setPermission', false);")
|
||||
|
||||
# Check that a page restriction has been created
|
||||
self.assertTrue(PageViewRestriction.objects.filter(page=self.public_page).exists())
|
||||
|
||||
# Check that the password is set correctly
|
||||
self.assertEqual(PageViewRestriction.objects.get(page=self.public_page).password, 'helloworld')
|
||||
|
||||
def test_set_password_restriction_password_unset(self):
|
||||
"""
|
||||
This tests that the password field on the form is validated correctly
|
||||
"""
|
||||
post_data = {
|
||||
'restriction_type': 'password',
|
||||
'password': '',
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_set_privacy', args=(self.public_page.id, )), post_data)
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'password', "This field is required.")
|
||||
|
||||
def test_unset_password_restriction(self):
|
||||
"""
|
||||
This tests that removing a password restriction using the set_privacy view works
|
||||
"""
|
||||
post_data = {
|
||||
'restriction_type': 'none',
|
||||
'password': '',
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_set_privacy', args=(self.private_page.id, )), post_data)
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "modal.respond('setPermission', true);")
|
||||
|
||||
# Check that the page restriction has been deleted
|
||||
self.assertFalse(PageViewRestriction.objects.filter(page=self.private_page).exists())
|
||||
|
||||
|
||||
class TestPrivacyIndicators(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
# Create some pages
|
||||
self.homepage = Page.objects.get(id=2)
|
||||
|
||||
self.public_page = self.homepage.add_child(instance=SimplePage(
|
||||
title="Public page",
|
||||
slug='public-page',
|
||||
live=True,
|
||||
))
|
||||
|
||||
self.private_page = self.homepage.add_child(instance=SimplePage(
|
||||
title="Private page",
|
||||
slug='private-page',
|
||||
live=True,
|
||||
))
|
||||
PageViewRestriction.objects.create(page=self.private_page, password='password123')
|
||||
|
||||
self.private_child_page = self.private_page.add_child(instance=SimplePage(
|
||||
title="Private child page",
|
||||
slug='private-child-page',
|
||||
live=True,
|
||||
))
|
||||
|
||||
def test_explorer_public(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the public pages explore view is set to "PUBLIC"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.public_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator public">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator private">')
|
||||
|
||||
def test_explorer_private(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the private pages explore view is set to "PRIVATE"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.private_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator private">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator public">')
|
||||
|
||||
def test_explorer_private_child(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the private child pages explore view is set to "PRIVATE"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.private_child_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator private">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator public">')
|
||||
|
||||
def test_explorer_list_homepage(self):
|
||||
"""
|
||||
This tests that there is a padlock displayed next to the private page in the homepages explorer listing
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.homepage.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Must have one privacy icon (next to the private page)
|
||||
self.assertContains(response, "<span class=\"privacy-indicator icon icon-locked\"", count=1)
|
||||
|
||||
def test_explorer_list_private(self):
|
||||
"""
|
||||
This tests that there is a padlock displayed next to the private child page in the private pages explorer listing
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.private_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Must have one privacy icon (next to the private child page)
|
||||
self.assertContains(response, "<span class=\"privacy-indicator icon icon-locked\"", count=1)
|
||||
|
||||
def test_edit_public(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the public pages edit view is set to "PUBLIC"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.public_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator public">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator private">')
|
||||
|
||||
def test_edit_private(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the private pages edit view is set to "PRIVATE"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.private_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator private">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator public">')
|
||||
|
||||
def test_edit_private_child(self):
|
||||
"""
|
||||
This tests that the privacy indicator on the private child pages edit view is set to "PRIVATE"
|
||||
"""
|
||||
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.private_child_page.id, )))
|
||||
|
||||
# Check the response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check the privacy indicator is public
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/pages/_privacy_indicator.html')
|
||||
self.assertContains(response, '<div class="privacy-indicator private">')
|
||||
self.assertNotContains(response, '<div class="privacy-indicator public">')
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from wagtail.wagtailadmin.forms import PasswordResetForm
|
||||
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar
|
||||
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar, page_privacy
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
|
||||
|
|
@ -67,6 +67,8 @@ urlpatterns += [
|
|||
url(r'^pages/moderation/(\d+)/reject/$', pages.reject_moderation, name='wagtailadmin_pages_reject_moderation'),
|
||||
url(r'^pages/moderation/(\d+)/preview/$', pages.preview_for_moderation, name='wagtailadmin_pages_preview_for_moderation'),
|
||||
|
||||
url(r'^pages/(\d+)/privacy/$', page_privacy.set_privacy, name='wagtailadmin_pages_set_privacy'),
|
||||
|
||||
url(r'^choose-page/$', chooser.browse, name='wagtailadmin_choose_page'),
|
||||
url(r'^choose-page/(\d+)/$', chooser.browse, name='wagtailadmin_choose_page_child'),
|
||||
url(r'^choose-external-link/$', chooser.external_link, name='wagtailadmin_choose_page_external_link'),
|
||||
|
|
|
|||
77
wagtail/wagtailadmin/views/page_privacy.py
Normal file
77
wagtail/wagtailadmin/views/page_privacy.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from wagtail.wagtailcore.models import Page, PageViewRestriction
|
||||
from wagtail.wagtailadmin.forms import PageViewRestrictionForm
|
||||
from wagtail.wagtailadmin.modal_workflow import render_modal_workflow
|
||||
|
||||
@permission_required('wagtailadmin.access_admin')
|
||||
def set_privacy(request, page_id):
|
||||
page = get_object_or_404(Page, id=page_id)
|
||||
page_perms = page.permissions_for_user(request.user)
|
||||
if not page_perms.can_set_view_restrictions():
|
||||
raise PermissionDenied
|
||||
|
||||
# fetch restriction records in depth order so that ancestors appear first
|
||||
restrictions = page.get_view_restrictions().order_by('page__depth')
|
||||
if restrictions:
|
||||
restriction = restrictions[0]
|
||||
restriction_exists_on_ancestor = (restriction.page != page)
|
||||
else:
|
||||
restriction = None
|
||||
restriction_exists_on_ancestor = False
|
||||
|
||||
if request.POST:
|
||||
form = PageViewRestrictionForm(request.POST)
|
||||
if form.is_valid() and not restriction_exists_on_ancestor:
|
||||
if form.cleaned_data['restriction_type'] == 'none':
|
||||
# remove any existing restriction
|
||||
if restriction:
|
||||
restriction.delete()
|
||||
else: # restriction_type = 'password'
|
||||
if restriction:
|
||||
restriction.password = form.cleaned_data['password']
|
||||
restriction.save()
|
||||
else:
|
||||
# create a new restriction object
|
||||
PageViewRestriction.objects.create(
|
||||
page=page, password = form.cleaned_data['password'])
|
||||
|
||||
return render_modal_workflow(
|
||||
request, None, 'wagtailadmin/page_privacy/set_privacy_done.js', {
|
||||
'is_public': (form.cleaned_data['restriction_type'] == 'none')
|
||||
}
|
||||
)
|
||||
|
||||
else: # request is a GET
|
||||
if not restriction_exists_on_ancestor:
|
||||
if restriction:
|
||||
form = PageViewRestrictionForm(initial={
|
||||
'restriction_type': 'password', 'password': restriction.password
|
||||
})
|
||||
else:
|
||||
# no current view restrictions on this page
|
||||
form = PageViewRestrictionForm(initial={
|
||||
'restriction_type': 'none'
|
||||
})
|
||||
|
||||
if restriction_exists_on_ancestor:
|
||||
# display a message indicating that there is a restriction at ancestor level -
|
||||
# do not provide the form for setting up new restrictions
|
||||
return render_modal_workflow(
|
||||
request, 'wagtailadmin/page_privacy/ancestor_privacy.html', None,
|
||||
{
|
||||
'page_with_restriction': restriction.page,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# no restriction set at ancestor level - can set restrictions here
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
'wagtailadmin/page_privacy/set_privacy.html',
|
||||
'wagtailadmin/page_privacy/set_privacy.js', {
|
||||
'page': page,
|
||||
'form': form,
|
||||
}
|
||||
)
|
||||
16
wagtail/wagtailcore/forms.py
Normal file
16
wagtail/wagtailcore/forms.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from django import forms
|
||||
|
||||
class PasswordPageViewRestrictionForm(forms.Form):
|
||||
password = forms.CharField(label="Password", widget=forms.PasswordInput)
|
||||
return_url = forms.CharField(widget=forms.HiddenInput)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.restriction = kwargs.pop('instance')
|
||||
super(PasswordPageViewRestrictionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_password(self):
|
||||
data = self.cleaned_data['password']
|
||||
if data != self.restriction.password:
|
||||
raise forms.ValidationError("The password you have entered is not correct. Please try again.")
|
||||
|
||||
return data
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'PageViewRestriction'
|
||||
db.create_table('wagtailcore_pageviewrestriction', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='view_restrictions', to=orm['wagtailcore.Page'])),
|
||||
('password', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
))
|
||||
db.send_create_signal('wagtailcore', ['PageViewRestriction'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'PageViewRestriction'
|
||||
db.delete_table('wagtailcore_pageviewrestriction')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'wagtailcore.grouppagepermission': {
|
||||
'Meta': {'object_name': 'GroupPagePermission'},
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
'wagtailcore.page': {
|
||||
'Meta': {'object_name': 'Page'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['contenttypes.ContentType']"}),
|
||||
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'expire_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.pagerevision': {
|
||||
'Meta': {'object_name': 'PageRevision'},
|
||||
'approved_go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'content_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.pageviewrestriction': {
|
||||
'Meta': {'object_name': 'PageViewRestriction'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'view_restrictions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
'wagtailcore.site': {
|
||||
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
|
||||
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': "orm['wagtailcore.Page']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['wagtailcore']
|
||||
|
|
@ -27,6 +27,7 @@ from treebeard.mp_tree import MP_Node
|
|||
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore
|
||||
from wagtail.wagtailcore.query import PageQuerySet
|
||||
from wagtail.wagtailcore.url_routing import RouteResult
|
||||
|
||||
from wagtail.wagtailsearch import indexed
|
||||
from wagtail.wagtailsearch.backends import get_search_backend
|
||||
|
|
@ -228,6 +229,12 @@ class PageManager(models.Manager):
|
|||
def not_type(self, model):
|
||||
return self.get_queryset().not_type(model)
|
||||
|
||||
def public(self):
|
||||
return self.get_queryset().public()
|
||||
|
||||
def not_public(self):
|
||||
return self.get_queryset().not_public()
|
||||
|
||||
|
||||
class PageBase(models.base.ModelBase):
|
||||
"""Metaclass for Page"""
|
||||
|
|
@ -406,7 +413,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
|
|||
else:
|
||||
# request is for this very page
|
||||
if self.live:
|
||||
return self.serve(request)
|
||||
return RouteResult(self)
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
|
|
@ -800,6 +807,24 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
|
|||
def get_prev_siblings(self, inclusive=False):
|
||||
return self.get_siblings(inclusive).filter(path__lte=self.path).order_by('-path')
|
||||
|
||||
def get_view_restrictions(self):
|
||||
"""Return a query set of all page view restrictions that apply to this page"""
|
||||
return PageViewRestriction.objects.filter(page__in=self.get_ancestors(inclusive=True))
|
||||
|
||||
password_required_template = getattr(settings, 'PASSWORD_REQUIRED_TEMPLATE', 'wagtailcore/password_required.html')
|
||||
def serve_password_required_response(self, request, form, action_url):
|
||||
"""
|
||||
Serve a response indicating that the user has been denied access to view this page,
|
||||
and must supply a password.
|
||||
form = a Django form object containing the password input
|
||||
(and zero or more hidden fields that also need to be output on the template)
|
||||
action_url = URL that this form should be POSTed to
|
||||
"""
|
||||
context = self.get_context(request)
|
||||
context['form'] = form
|
||||
context['action_url'] = action_url
|
||||
return TemplateResponse(request, self.password_required_template, context)
|
||||
|
||||
|
||||
def get_navigation_menu_items():
|
||||
# Get all pages that appear in the navigation menu: ones which have children,
|
||||
|
|
@ -1084,6 +1109,9 @@ class PagePermissionTester(object):
|
|||
|
||||
return self.user.is_superuser or ('publish' in self.permissions)
|
||||
|
||||
def can_set_view_restrictions(self):
|
||||
return self.can_publish()
|
||||
|
||||
def can_publish_subpage(self):
|
||||
"""
|
||||
Niggly special case for creating and publishing a page in one go.
|
||||
|
|
@ -1141,3 +1169,8 @@ class PagePermissionTester(object):
|
|||
else:
|
||||
# no publishing required, so the already-tested 'add' permission is sufficient
|
||||
return True
|
||||
|
||||
|
||||
class PageViewRestriction(models.Model):
|
||||
page = models.ForeignKey('Page', related_name='view_restrictions')
|
||||
password = models.CharField(max_length=255)
|
||||
|
|
|
|||
|
|
@ -107,3 +107,17 @@ class PageQuerySet(MP_NodeQuerySet):
|
|||
|
||||
def not_type(self, model):
|
||||
return self.exclude(self.type_q(model))
|
||||
|
||||
def public_q(self):
|
||||
from wagtail.wagtailcore.models import PageViewRestriction
|
||||
|
||||
q = Q()
|
||||
for restriction in PageViewRestriction.objects.all():
|
||||
q &= ~self.descendant_of_q(restriction.page, inclusive=True)
|
||||
return q
|
||||
|
||||
def public(self):
|
||||
return self.filter(self.public_q())
|
||||
|
||||
def not_public(self):
|
||||
return self.exclude(self.public_q())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Password required</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Password required</h1>
|
||||
<p>You need a password to access this page.</p>
|
||||
<form action="{{ action_url }}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Continue" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import warnings
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage, PageWithOldStyleRouteMethod
|
||||
|
||||
|
||||
class TestSiteRouting(TestCase):
|
||||
|
|
@ -136,8 +138,13 @@ class TestRouting(TestCase):
|
|||
|
||||
request = HttpRequest()
|
||||
request.path = '/events/christmas/'
|
||||
response = homepage.route(request, ['events', 'christmas'])
|
||||
(found_page, args, kwargs) = homepage.route(request, ['events', 'christmas'])
|
||||
self.assertEqual(found_page, christmas_page)
|
||||
|
||||
def test_request_serving(self):
|
||||
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
|
||||
request = HttpRequest()
|
||||
response = christmas_page.serve(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context_data['self'], christmas_page)
|
||||
used_template = response.resolve_template(response.template_name)
|
||||
|
|
@ -226,6 +233,28 @@ class TestServeView(TestCase):
|
|||
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
|
||||
|
||||
|
||||
def test_old_style_routing(self):
|
||||
"""
|
||||
Test that route() methods that return an HttpResponse are correctly handled
|
||||
"""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
response = self.client.get('/old-style-route/')
|
||||
|
||||
# Check that a DeprecationWarning has been triggered
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("Page.route should return an instance of wagtailcore.url_routing.RouteResult" in str(w[-1].message))
|
||||
|
||||
expected_page = PageWithOldStyleRouteMethod.objects.get(url_path='/home/old-style-route/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context['self'], expected_page)
|
||||
self.assertEqual(response.templates[0].name, 'tests/simple_page.html')
|
||||
|
||||
def test_before_serve_hook(self):
|
||||
response = self.client.get('/events/', HTTP_USER_AGENT='GoogleBot')
|
||||
self.assertContains(response, 'bad googlebot no cookie')
|
||||
|
||||
|
||||
class TestStaticSitePaths(TestCase):
|
||||
def setUp(self):
|
||||
self.root_page = Page.objects.get(id=1)
|
||||
|
|
|
|||
70
wagtail/wagtailcore/tests/test_page_privacy.py
Normal file
70
wagtail/wagtailcore/tests/test_page_privacy.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from django.test import TestCase
|
||||
from wagtail.wagtailcore.models import Page, PageViewRestriction
|
||||
|
||||
|
||||
class TestPagePrivacy(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def setUp(self):
|
||||
self.secret_plans_page = Page.objects.get(url_path='/home/secret-plans/')
|
||||
self.view_restriction = PageViewRestriction.objects.get(
|
||||
page=self.secret_plans_page)
|
||||
|
||||
def test_anonymous_user_must_authenticate(self):
|
||||
response = self.client.get('/secret-plans/')
|
||||
self.assertEqual(response.templates[0].name, 'wagtailcore/password_required.html')
|
||||
|
||||
submit_url = "/_util/authenticate_with_password/%d/%d/" % (self.view_restriction.id, self.secret_plans_page.id)
|
||||
self.assertContains(response, '<form action="%s"' % submit_url)
|
||||
self.assertContains(response, '<input id="id_return_url" name="return_url" type="hidden" value="/secret-plans/" />')
|
||||
|
||||
# posting the wrong password should redisplay the password page
|
||||
response = self.client.post(submit_url, {
|
||||
'password': 'wrongpassword',
|
||||
'return_url': '/secret-plans/',
|
||||
})
|
||||
self.assertEqual(response.templates[0].name, 'wagtailcore/password_required.html')
|
||||
self.assertContains(response, '<form action="%s"' % submit_url)
|
||||
|
||||
# posting the correct password should redirect back to return_url
|
||||
response = self.client.post(submit_url, {
|
||||
'password': 'swordfish',
|
||||
'return_url': '/secret-plans/',
|
||||
})
|
||||
self.assertRedirects(response, '/secret-plans/')
|
||||
|
||||
# now requests to /secret-plans/ should pass authentication
|
||||
response = self.client.get('/secret-plans/')
|
||||
self.assertEqual(response.templates[0].name, 'tests/simple_page.html')
|
||||
|
||||
|
||||
def test_view_restrictions_apply_to_subpages(self):
|
||||
underpants_page = Page.objects.get(url_path='/home/secret-plans/steal-underpants/')
|
||||
response = self.client.get('/secret-plans/steal-underpants/')
|
||||
|
||||
# check that we're overriding the default password_required template for this page type
|
||||
self.assertEqual(response.templates[0].name, 'tests/event_page_password_required.html')
|
||||
|
||||
submit_url = "/_util/authenticate_with_password/%d/%d/" % (self.view_restriction.id, underpants_page.id)
|
||||
self.assertContains(response, '<title>Steal underpants</title>')
|
||||
self.assertContains(response, '<form action="%s"' % submit_url)
|
||||
self.assertContains(response, '<input id="id_return_url" name="return_url" type="hidden" value="/secret-plans/steal-underpants/" />')
|
||||
|
||||
# posting the wrong password should redisplay the password page
|
||||
response = self.client.post(submit_url, {
|
||||
'password': 'wrongpassword',
|
||||
'return_url': '/secret-plans/steal-underpants/',
|
||||
})
|
||||
self.assertEqual(response.templates[0].name, 'tests/event_page_password_required.html')
|
||||
self.assertContains(response, '<form action="%s"' % submit_url)
|
||||
|
||||
# posting the correct password should redirect back to return_url
|
||||
response = self.client.post(submit_url, {
|
||||
'password': 'swordfish',
|
||||
'return_url': '/secret-plans/steal-underpants/',
|
||||
})
|
||||
self.assertRedirects(response, '/secret-plans/steal-underpants/')
|
||||
|
||||
# now requests to /secret-plans/ should pass authentication
|
||||
response = self.client.get('/secret-plans/steal-underpants/')
|
||||
self.assertEqual(response.templates[0].name, 'tests/event_page.html')
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailcore.models import Page, PageViewRestriction
|
||||
from wagtail.tests.models import EventPage
|
||||
|
||||
|
||||
|
|
@ -270,3 +270,43 @@ class TestPageQuerySet(TestCase):
|
|||
# Check that the homepage is in the results
|
||||
homepage = Page.objects.get(url_path='/home/')
|
||||
self.assertTrue(pages.filter(id=homepage.id).exists())
|
||||
|
||||
def test_public(self):
|
||||
events_index = Page.objects.get(url_path='/home/events/')
|
||||
event = Page.objects.get(url_path='/home/events/christmas/')
|
||||
homepage = Page.objects.get(url_path='/home/')
|
||||
|
||||
# Add PageViewRestriction to events_index
|
||||
PageViewRestriction.objects.create(page=events_index, password='hello')
|
||||
|
||||
# Get public pages
|
||||
pages = Page.objects.public()
|
||||
|
||||
# Check that the homepage is in the results
|
||||
self.assertTrue(pages.filter(id=homepage.id).exists())
|
||||
|
||||
# Check that the events index is not in the results
|
||||
self.assertFalse(pages.filter(id=events_index.id).exists())
|
||||
|
||||
# Check that the event is not in the results
|
||||
self.assertFalse(pages.filter(id=event.id).exists())
|
||||
|
||||
def test_not_public(self):
|
||||
events_index = Page.objects.get(url_path='/home/events/')
|
||||
event = Page.objects.get(url_path='/home/events/christmas/')
|
||||
homepage = Page.objects.get(url_path='/home/')
|
||||
|
||||
# Add PageViewRestriction to events_index
|
||||
PageViewRestriction.objects.create(page=events_index, password='hello')
|
||||
|
||||
# Get public pages
|
||||
pages = Page.objects.not_public()
|
||||
|
||||
# Check that the homepage is not in the results
|
||||
self.assertFalse(pages.filter(id=homepage.id).exists())
|
||||
|
||||
# Check that the events index is in the results
|
||||
self.assertTrue(pages.filter(id=events_index.id).exists())
|
||||
|
||||
# Check that the event is in the results
|
||||
self.assertTrue(pages.filter(id=event.id).exists())
|
||||
|
|
|
|||
15
wagtail/wagtailcore/url_routing.py
Normal file
15
wagtail/wagtailcore/url_routing.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class RouteResult(object):
|
||||
"""
|
||||
An object to be returned from Page.route, which encapsulates
|
||||
all the information necessary to serve an HTTP response. Analogous to
|
||||
django.core.urlresolvers.ResolverMatch, except that it identifies
|
||||
a Page instance that we will call serve(*args, **kwargs) on, rather
|
||||
than a view function.
|
||||
"""
|
||||
def __init__(self, page, args=None, kwargs=None):
|
||||
self.page = page
|
||||
self.args = args or []
|
||||
self.kwargs = kwargs or {}
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.page, self.args, self.kwargs)[index]
|
||||
|
|
@ -2,7 +2,10 @@ from django.conf.urls import url
|
|||
from wagtail.wagtailcore import views
|
||||
|
||||
urlpatterns = [
|
||||
# All front-end views are handled through Wagtail's core.views.serve mechanism.
|
||||
url(r'^_util/authenticate_with_password/(\d+)/(\d+)/$', views.authenticate_with_password,
|
||||
name='wagtailcore_authenticate_with_password'),
|
||||
|
||||
# Front-end page views are handled through Wagtail's core.views.serve mechanism.
|
||||
# Here we match a (possibly empty) list of path segments, each followed by
|
||||
# a '/'. If a trailing slash is not present, we leave CommonMiddleware to
|
||||
# handle it as usual (i.e. redirect it to the trailing slash version if
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
from django.http import Http404
|
||||
import warnings
|
||||
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, PageViewRestriction
|
||||
from wagtail.wagtailcore.forms import PasswordPageViewRestrictionForm
|
||||
|
||||
|
||||
def serve(request, path):
|
||||
|
|
@ -8,4 +17,47 @@ def serve(request, path):
|
|||
raise Http404
|
||||
|
||||
path_components = [component for component in path.split('/') if component]
|
||||
return request.site.root_page.specific.route(request, path_components)
|
||||
route_result = request.site.root_page.specific.route(request, path_components)
|
||||
if isinstance(route_result, HttpResponse):
|
||||
warnings.warn(
|
||||
"Page.route should return an instance of wagtailcore.url_routing.RouteResult, not an HttpResponse",
|
||||
DeprecationWarning
|
||||
)
|
||||
return route_result
|
||||
|
||||
(page, args, kwargs) = route_result
|
||||
for fn in hooks.get_hooks('before_serve_page'):
|
||||
result = fn(page, request, args, kwargs)
|
||||
if isinstance(result, HttpResponse):
|
||||
return result
|
||||
|
||||
return page.serve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def authenticate_with_password(request, page_view_restriction_id, page_id):
|
||||
"""
|
||||
Handle a submission of PasswordPageViewRestrictionForm to grant view access over a
|
||||
subtree that is protected by a PageViewRestriction
|
||||
"""
|
||||
restriction = get_object_or_404(PageViewRestriction, id=page_view_restriction_id)
|
||||
page = get_object_or_404(Page, id=page_id).specific
|
||||
|
||||
if request.POST:
|
||||
form = PasswordPageViewRestrictionForm(request.POST, instance=restriction)
|
||||
if form.is_valid():
|
||||
has_existing_session = (settings.SESSION_COOKIE_NAME in request.COOKIES)
|
||||
passed_restrictions = request.session.setdefault('passed_page_view_restrictions', [])
|
||||
if restriction.id not in passed_restrictions:
|
||||
passed_restrictions.append(restriction.id)
|
||||
request.session['passed_page_view_restrictions'] = passed_restrictions
|
||||
if not has_existing_session:
|
||||
# if this is a session we've created, set it to expire at the end
|
||||
# of the browser session
|
||||
request.session.set_expiry(0)
|
||||
|
||||
return redirect(form.cleaned_data['return_url'])
|
||||
else:
|
||||
form = PasswordPageViewRestrictionForm(instance=restriction)
|
||||
|
||||
action_url = reverse('wagtailcore_authenticate_with_password', args=[restriction.id, page.id])
|
||||
return page.serve_password_required_response(request, form, action_url)
|
||||
|
|
|
|||
25
wagtail/wagtailcore/wagtail_hooks.py
Normal file
25
wagtail/wagtailcore/wagtail_hooks.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def check_view_restrictions(page, request, serve_args, serve_kwargs):
|
||||
"""
|
||||
Check whether there are any view restrictions on this page which are
|
||||
not fulfilled by the given request object. If there are, return an
|
||||
HttpResponse that will notify the user of that restriction (and possibly
|
||||
include a password / login form that will allow them to proceed). If
|
||||
there are no such restrictions, return None
|
||||
"""
|
||||
restrictions = page.get_view_restrictions()
|
||||
|
||||
if restrictions:
|
||||
passed_restrictions = request.session.get('passed_page_view_restrictions', [])
|
||||
for restriction in restrictions:
|
||||
if restriction.id not in passed_restrictions:
|
||||
from wagtail.wagtailcore.forms import PasswordPageViewRestrictionForm
|
||||
form = PasswordPageViewRestrictionForm(instance=restriction,
|
||||
initial={'return_url': request.get_full_path()})
|
||||
action_url = reverse('wagtailcore_authenticate_with_password', args=[restriction.id, page.id])
|
||||
return page.serve_password_required_response(request, form, action_url)
|
||||
|
||||
hooks.register('before_serve_page', check_view_restrictions)
|
||||
Loading…
Reference in a new issue