diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index a35287e158..52d90904a2 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -15,7 +15,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.utils.datastructures import MultiValueDict from django.utils.encoding import iri_to_uri, force_unicode, smart_str -from django.utils.functional import memoize +from django.utils.functional import memoize, lazy from django.utils.importlib import import_module from django.utils.regex_helper import normalize @@ -390,6 +390,8 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view, *args, **kwargs))) +reverse_lazy = lazy(reverse, str) + def clear_url_caches(): global _resolver_cache global _callable_cache diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index a63fff7295..e86e07031a 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -37,6 +37,12 @@ compatibility with old browsers, this change means that you can use any HTML5 features you need in admin pages without having to lose HTML validity or override the provided templates to change the doctype. +``reverse_lazy`` +~~~~~~~~~~~~~~~~ + +A lazily evaluated version of :func:`django.core.urlresolvers.reverse` was +added to allow using URL reversals before the project's URLConf gets loaded. + .. _backwards-incompatible-changes-1.4: Backwards incompatible changes in 1.4 diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index d721012d3e..ae1ca3f254 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -827,6 +827,26 @@ namespaces into URLs on specific application instances, according to the ``urllib.quote``) to the ouput of :meth:`~django.core.urlresolvers.reverse` may produce undesirable results. +reverse_lazy() +-------------- + +.. versionadded:: 1.4 + +A lazily evaluated version of `reverse()`_. + +It is useful for when you need to use a URL reversal before your project's +URLConf is loaded. Some common cases where this method is necessary are:: + +* providing a reversed URL as the ``url`` attribute of a generic class-based + view. + +* providing a reversed URL to a decorator (such as the ``login_url`` argument + for the :func:`django.contrib.auth.decorators.permission_required` + decorator). + +* providing a reversed URL as a default value for a parameter in a function's + signature. + resolve() --------- diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 198d55620c..4f391d9653 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.test import TestCase from django.utils import unittest +from django.contrib.auth.models import User import urlconf_outer import urlconf_inner @@ -218,6 +219,21 @@ class ResolverTests(unittest.TestCase): else: self.assertEqual(t.name, e['name'], 'Wrong URL name. Expected "%s", got "%s".' % (e['name'], t.name)) +class ReverseLazyTest(TestCase): + urls = 'regressiontests.urlpatterns_reverse.reverse_lazy_urls' + + def test_redirect_with_lazy_reverse(self): + response = self.client.get('/redirect/') + self.assertRedirects(response, "/redirected_to/", status_code=301) + + def test_user_permission_with_lazy_reverse(self): + user = User.objects.create_user('alfred', 'alfred@example.com', password='testpw') + response = self.client.get('/login_required_view/') + self.assertRedirects(response, "/login/?next=/login_required_view/", status_code=302) + self.client.login(username='alfred', password='testpw') + response = self.client.get('/login_required_view/') + self.assertEqual(response.status_code, 200) + class ReverseShortcutTests(TestCase): urls = 'regressiontests.urlpatterns_reverse.urls' diff --git a/tests/regressiontests/urlpatterns_reverse/views.py b/tests/regressiontests/urlpatterns_reverse/views.py index fdd742382c..b04bf33ff1 100644 --- a/tests/regressiontests/urlpatterns_reverse/views.py +++ b/tests/regressiontests/urlpatterns_reverse/views.py @@ -1,4 +1,8 @@ from django.http import HttpResponse +from django.views.generic import RedirectView +from django.core.urlresolvers import reverse_lazy + +from django.contrib.auth.decorators import user_passes_test def empty_view(request, *args, **kwargs): return HttpResponse('') @@ -15,5 +19,12 @@ class ViewClass(object): view_class_instance = ViewClass() +class LazyRedictView(RedirectView): + url = reverse_lazy('named-lazy-url-redirected-to') + +@user_passes_test(lambda u: u.is_authenticated(), login_url=reverse_lazy('some-login-page')) +def login_required_view(request): + return HttpResponse('Hello you') + def bad_view(request, *args, **kwargs): raise ValueError("I don't think I'm getting good value for this view")