diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 555fd98fc6236..37053cee2031f 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -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 diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index c3d93bb247644..e324b442ae3bd 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -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: @@ -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): diff --git a/django/views/decorators/predicate.py b/django/views/decorators/predicate.py new file mode 100644 index 0000000000000..4c4fae065e647 --- /dev/null +++ b/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 + diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt index 25616a44c016d..3bf4952fd0acb 100644 --- a/docs/topics/http/decorators.txt +++ b/docs/topics/http/decorators.txt @@ -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 =========================== diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 3680eecdd2f59..fc4f9bcb2d545 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -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) diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 29858055abd4a..9804449f6be57 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -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), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py index 9cc86ae6f3a00..4726e27d8e972 100644 --- a/tests/handlers/views.py +++ b/tests/handlers/views.py @@ -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") @@ -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")