Added @route decorator

This commit is contained in:
Karl Hobley 2015-04-08 13:51:29 +01:00 committed by Matt Westcott
parent f7b2cfbb4e
commit 7200def791
5 changed files with 175 additions and 51 deletions

View file

@ -1,49 +1,46 @@
.. _routable_page_mixin:
====================================
Embedding URL configuration in Pages
====================================
==============
Routable pages
==============
The ``RoutablePageMixin`` mixin provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like ``/blog/2013/06/``, ``/blog/authors/bob/``, ``/blog/tagged/python/``, all served by the same ``BlogIndex`` page.
The ``RoutablePageMixin`` mixin provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like ``/blog/2013/06/``, ``/blog/authors/bob/``, ``/blog/tagged/python/``, all served by the same page instance.
A ``Page`` using ``RoutablePageMixin`` exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns, using Django's urlconf scheme. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).
A ``Page`` using ``RoutablePageMixin`` exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).
The basics
==========
To use ``RoutablePageMixin``, you need to make your class inherit from both :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin` and :class:`wagtail.wagtailcore.models.Page`, and configure the ``subpage_urls`` attribute with your URL configuration.
To use ``RoutablePageMixin``, you need to make your class inherits from both :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin` and :class:`wagtail.wagtailcore.models.Page`, then define some view methods and decorate them with ``wagtail.contrib.wagtailroutablepage.models.route``.
Here's an example of an ``EventPage`` with three views:
.. code-block:: python
from django.conf.urls import url
from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin
from wagtail.wagtailcore.models import Page
from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin, route
class EventPage(RoutablePageMixin, Page):
subpage_urls = (
url(r'^$', 'current_events', name='current_events'),
url(r'^past/$', 'past_events', name='past_events'),
url(r'^year/(\d+)/$', 'events_for_year', name='events_for_year'),
)
...
@route(r'^$')
def current_events(self, request):
"""
View function for the current events page
"""
...
@route(r'^past/$')
def past_events(self, request):
"""
View function for the past events page
"""
...
def events_for_year(self, request):
@route(r'^year/(\d+)/$')
def events_for_year(self, request, year):
"""
View function for the events for year page
"""
@ -56,29 +53,7 @@ The ``RoutablePageMixin`` class
.. automodule:: wagtail.contrib.wagtailroutablepage.models
.. autoclass:: RoutablePageMixin
.. autoattribute:: subpage_urls
Example:
.. code-block:: python
from django.conf.urls import url
from wagtail.wagtailcore.models import Page
class MyPage(RoutablePageMixin, Page):
subpage_urls = (
url(r'^$', 'main', name='main'),
url(r'^archive/$', 'archive', name='archive'),
url(r'^archive/(?P<year>[0-9]{4})/$', 'archive', name='archive'),
)
def main(self, request):
...
def archive(self, request, year=None):
...
.. automethod:: get_subpage_urls
.. automethod:: resolve_subpage

View file

@ -4,11 +4,35 @@ from six import string_types
from django.http import Http404
from django.core.urlresolvers import RegexURLResolver
from django.conf.urls import url
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.url_routing import RouteResult
_creation_counter = 0
def route(pattern, name=None):
def decorator(view_func):
global _creation_counter
_creation_counter += 1
# Make sure page has _routablepage_routes attribute
if not hasattr(view_func, '_routablepage_routes'):
view_func._routablepage_routes = []
# Add new route to view
view_func._routablepage_routes.append((
url(pattern, view_func, name=(name or view_func.__name__)),
_creation_counter,
))
return view_func
return decorator
class RoutablePageMixin(object):
"""
This class can be mixed in to a Page subclass to allow urlconfs to be
@ -19,10 +43,21 @@ class RoutablePageMixin(object):
@classmethod
def get_subpage_urls(cls):
# Old style
if cls.subpage_urls:
return cls.subpage_urls
return ()
# New style
routes = []
for attr in dir(cls):
val = getattr(cls, attr)
if hasattr(val, '_routablepage_routes'):
routes.extend(val._routablepage_routes)
return tuple([
route[0]
for route in sorted(routes, key=lambda route: route[1])
])
@classmethod
def get_resolver(cls):
@ -48,9 +83,14 @@ class RoutablePageMixin(object):
"""
view, args, kwargs = self.get_resolver().resolve(path)
# If view is a string, find it as an attribute of self
if isinstance(view, string_types):
view = getattr(self, view)
if self.subpage_urls: # Old style
# If view is a string, find it as an attribute of self
if isinstance(view, string_types):
view = getattr(self, view)
else: # New style
# Bind the method
view = view.__get__(self, type(self))
return view, args, kwargs

View file

@ -1,14 +1,17 @@
from django.test import TestCase, RequestFactory
from django.core.urlresolvers import NoReverseMatch
from wagtail.wagtailcore.models import Page, Site
from wagtail.tests.routablepage.models import RoutablePageTest, routable_page_external_view
from wagtail.tests.routablepage.models import OldStyleRoutablePageTest, NewStyleRoutablePageTest, routable_page_external_view
from wagtail.contrib.wagtailroutablepage.templatetags.wagtailroutablepage_tags import routablepageurl
class TestRoutablePage(TestCase):
class TestNewStyleRoutablePage(TestCase):
model = NewStyleRoutablePageTest
def setUp(self):
self.home_page = Page.objects.get(id=2)
self.routable_page = self.home_page.add_child(instance=RoutablePageTest(
self.routable_page = self.home_page.add_child(instance=self.model(
title="Routable Page",
slug='routable-page',
live=True,
@ -38,10 +41,17 @@ class TestRoutablePage(TestCase):
def test_resolve_external_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/external/joe-bloggs/')
self.assertEqual(view, routable_page_external_view)
self.assertEqual(view, self.routable_page.external_view)
self.assertEqual(args, ('joe-bloggs', ))
self.assertEqual(kwargs, {})
def test_resolve_external_view_other_route(self):
view, args, kwargs = self.routable_page.resolve_subpage('/external-no-arg/')
self.assertEqual(view, self.routable_page.external_view)
self.assertEqual(args, ())
self.assertEqual(kwargs, {})
def test_reverse_main_view(self):
url = self.routable_page.reverse_subpage('main')
@ -57,11 +67,25 @@ class TestRoutablePage(TestCase):
self.assertEqual(url, 'archive/author/joe-bloggs/')
def test_reverse_overridden_name(self):
url = self.routable_page.reverse_subpage('name_overridden')
self.assertEqual(url, 'override-name-test/')
def test_reverse_overridden_name_default_doesnt_work(self):
with self.assertRaises(NoReverseMatch):
self.routable_page.reverse_subpage('override_name_test')
def test_reverse_external_view(self):
url = self.routable_page.reverse_subpage('external_view', args=('joe-bloggs', ))
self.assertEqual(url, 'external/joe-bloggs/')
def test_reverse_external_view_other_route(self):
url = self.routable_page.reverse_subpage('external_view')
self.assertEqual(url, 'external-no-arg/')
def test_get_main_view(self):
response = self.client.get(self.routable_page.url)
@ -82,10 +106,39 @@ class TestRoutablePage(TestCase):
self.assertContains(response, "EXTERNAL VIEW: joe-bloggs")
def test_get_external_view_other_route(self):
response = self.client.get(self.routable_page.url + 'external-no-arg/')
class TestRoutablePageTemplateTag(TestRoutablePage):
self.assertContains(response, "EXTERNAL VIEW: ARG NOT SET")
class TestOldStyleRoutablePage(TestNewStyleRoutablePage):
model = OldStyleRoutablePageTest
def test_resolve_external_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/external/joe-bloggs/')
self.assertEqual(view, routable_page_external_view)
self.assertEqual(args, ('joe-bloggs', ))
self.assertEqual(kwargs, {})
test_resolve_external_view_other_route = None
test_reverse_external_view_other_route = None
test_get_external_view_other_route = None
test_reverse_overridden_name = None
test_reverse_overridden_name_default_doesnt_work = None
class TestRoutablePageTemplateTag(TestCase):
def setUp(self):
super(TestRoutablePageTemplateTag, self).setUp()
self.home_page = Page.objects.get(id=2)
self.routable_page = self.home_page.add_child(instance=NewStyleRoutablePageTest(
title="Routable Page",
slug='routable-page',
live=True,
))
self.rf = RequestFactory()
self.request = self.rf.get(self.routable_page.url)
self.request.site = Site.find_for_request(self.request)

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import wagtail.contrib.wagtailroutablepage.models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0013_update_golive_expire_help_text'),
('routablepagetests', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='OldStyleRoutablePageTest',
fields=[
('page_ptr', models.OneToOneField(primary_key=True, auto_created=True, serialize=False, to='wagtailcore.Page', parent_link=True)),
],
options={
'abstract': False,
},
bases=(wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin, 'wagtailcore.page'),
),
migrations.RenameModel(
old_name='RoutablePageTest',
new_name='NewStyleRoutablePageTest',
),
]

View file

@ -1,13 +1,14 @@
from django.http import HttpResponse
from django.conf.urls import url
from wagtail.contrib.wagtailroutablepage.models import RoutablePage
from wagtail.contrib.wagtailroutablepage.models import RoutablePage, route
def routable_page_external_view(request, arg):
def routable_page_external_view(request, arg="ARG NOT SET"):
return HttpResponse("EXTERNAL VIEW: " + arg)
class RoutablePageTest(RoutablePage):
class OldStyleRoutablePageTest(RoutablePage):
subpage_urls = (
url(r'^$', 'main', name='main'),
url(r'^archive/year/(\d+)/$', 'archive_by_year', name='archive_by_year'),
@ -23,3 +24,28 @@ class RoutablePageTest(RoutablePage):
def main(self, request):
return HttpResponse("MAIN VIEW")
class NewStyleRoutablePageTest(RoutablePage):
@route(r'^$')
def main(self, request):
return HttpResponse("MAIN VIEW")
@route(r'^archive/year/(\d+)/$')
def archive_by_year(self, request, year):
return HttpResponse("ARCHIVE BY YEAR: " + str(year))
@route(r'^archive/author/(?P<author_slug>.+)/$')
def archive_by_author(self, request, author_slug):
return HttpResponse("ARCHIVE BY AUTHOR: " + author_slug)
@route(r'^external/(.+)/$')
@route(r'^external-no-arg/$')
def external_view(self, *args, **kwargs):
return routable_page_external_view(*args, **kwargs)
# By default, the method name would be used as the url name but when the
# "name" kwarg is specified, this should override the default.
@route(r'^override-name-test/$', name='name_overridden')
def override_name_test(self, request):
pass