Skip to content

Commit

Permalink
Add predicate decorator to not match specific view
Browse files Browse the repository at this point in the history
Predicates is concept brought back from Pyramid, Pylon.
It has the goal of allowing a better organizatio of your
function based view and get rid of redundant code as
`if request.method == 'POST`

Improve the doc about predicates

Change title about predicate in the docs

Fix predicate explanation in docs
  • Loading branch information
rach committed May 22, 2013
1 parent 01948e3 commit f52227b
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 3 deletions.
2 changes: 1 addition & 1 deletion django/core/handlers/base.py
Expand Up @@ -98,7 +98,7 @@ def get_response(self, request):
urlresolvers.set_urlconf(urlconf)
resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)

resolver_match = resolver.resolve(request.path_info)
resolver_match = resolver.resolve(request.path_info, request=request)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match

Expand Down
18 changes: 16 additions & 2 deletions django/core/urlresolvers.py
Expand Up @@ -311,7 +311,7 @@ def app_dict(self):
self._populate()
return self._app_dict[language_code]

def resolve(self, path):
def resolve(self, path, request=None):
tried = []
match = self.regex.search(path)
if match:
Expand All @@ -329,10 +329,24 @@ def resolve(self, path):
if sub_match:
sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
sub_match_dict.update(sub_match.kwargs)

# we check for the request to keep retro compatibility
# and if the view function has been decorated with
# predicates check before matching
if request and hasattr(sub_match.func, 'predicates'):
predicate_check = True
for predicate in sub_match.func.predicates:
if not predicate(request, *sub_match.args):
predicate_check = False
break

if not predicate_check:
continue

return ResolverMatch(sub_match.func, sub_match.args, sub_match_dict, sub_match.url_name, self.app_name or sub_match.app_name, [self.namespace] + sub_match.namespaces)
tried.append([pattern])
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path})
raise Resolver404({'path': path})

@property
def urlconf_module(self):
Expand Down
47 changes: 47 additions & 0 deletions django/views/decorators/predicate.py
@@ -0,0 +1,47 @@
"""
Decorators for views based on HTTP headers.
"""

import logging
from functools import wraps

from django.utils.decorators import decorator_from_middleware, available_attrs
from django.utils.http import http_date
from django.middleware.http import ConditionalGetMiddleware

logger = logging.getLogger('django.request')


def url_predicates(predicates_list):
"""
Decorators to make urls matchable if predicates return True
Usage::
url_pattern += url('/', my_view_GET)
url_pattern += url('/', my_view_POST)
...
predicate_GET = lambda request: request.method == 'GET'
predicate_POST = lambda request: request.method == 'POST'
@url_predicates([predicate_GET])
def my_view_GET(request):
# I can assume now that only GET requests get match to the url
# associated to this view
# ...
@url_predicates([predicate_POST])
def my_view_POST(request):
# I can assume now that only POST requests get match to the url
# associated to this view
# ...
"""
def decorator(func):
@wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs):
return func(request, *args, **kwargs)
inner.predicates = predicates_list
return inner
return decorator

36 changes: 36 additions & 0 deletions docs/topics/http/decorators.txt
Expand Up @@ -51,6 +51,42 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met.
such as link checkers, rely on HEAD requests, you might prefer
using ``require_safe`` instead of ``require_GET``.


Predicates url processing
=========================

The decorators in :mod:django.views.decorators.predicate can be used to restrict
the view selection based on a list of function result.
These decorators will attach a list of functions to be checked before returning
a match for the url.
If the predicates fail then the process continue the url resolving process.

.. function:: view_predicates(predicate_function_list)

Decorator to require that a view only match particular request. Usage::

url_pattern += url('/', my_view_GET)
url_pattern += url('/', my_view_POST)

...

predicate_GET = lambda request: request.method == 'GET'
predicate_POST = lambda request: request.method == 'POST'

@url_predicates([predicate_GET])
def my_view_GET(request):
# I can assume now that only GET requests get match to the url
# associated to this view
# ...

@url_predicates([predicate_POST])
def my_view_POST(request):
# I can assume now that only POST requests get match to the url
# associated to this view
# ...



Conditional view processing
===========================

Expand Down
12 changes: 12 additions & 0 deletions tests/handlers/tests.py
Expand Up @@ -89,3 +89,15 @@ def test_request_signals_streaming_response(self):
self.assertEqual(self.signals, ['started'])
self.assertEqual(b''.join(response.streaming_content), b"streaming content")
self.assertEqual(self.signals, ['started', 'finished'])


class PredicatesTests(TestCase):
urls = 'handlers.urls'

def test_request_predicates(self):
response = self.client.post('/predicate/')
self.assertEqual(response.content, b"predicate content")

def test_request_predicates_fail_return_404(self):
response = self.client.get('/predicate/')
self.assertEqual(response.status_code, 404)
1 change: 1 addition & 0 deletions tests/handlers/urls.py
Expand Up @@ -9,4 +9,5 @@
url(r'^streaming/$', views.streaming),
url(r'^in_transaction/$', views.in_transaction),
url(r'^not_in_transaction/$', views.not_in_transaction),
url(r'^predicate/$', views.predicate),
)
5 changes: 5 additions & 0 deletions tests/handlers/views.py
Expand Up @@ -2,6 +2,7 @@

from django.db import connection, transaction
from django.http import HttpResponse, StreamingHttpResponse
from django.views.decorators.predicate import url_predicates

def regular(request):
return HttpResponse(b"regular content")
Expand All @@ -15,3 +16,7 @@ def in_transaction(request):
@transaction.non_atomic_requests
def not_in_transaction(request):
return HttpResponse(str(connection.in_atomic_block))

@url_predicates([lambda request: request.method == 'POST',])
def predicate(request):
return HttpResponse(b"predicate content")

0 comments on commit f52227b

Please sign in to comment.