mirror of https://github.com/django/django.git
Fixed #15552 -- LOGIN_URL and LOGIN_REDIRECT_URL can take URLpattern names.
Thanks UloPe and Eric Florenzano for the patch, and Malcolm Tredinnick for review.
This commit is contained in:
parent
518c582966
commit
a78dd109e6
|
@ -8,6 +8,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.utils.decorators import available_attrs
|
from django.utils.decorators import available_attrs
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
from django.shortcuts import resolve_url
|
||||||
|
|
||||||
|
|
||||||
def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
|
def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
|
||||||
|
@ -23,11 +24,10 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
|
||||||
if test_func(request.user):
|
if test_func(request.user):
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
path = request.build_absolute_uri()
|
path = request.build_absolute_uri()
|
||||||
# urlparse chokes on lazy objects in Python 3
|
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
|
||||||
login_url_as_str = force_str(login_url or settings.LOGIN_URL)
|
|
||||||
# If the login url is the same scheme and net location then just
|
# If the login url is the same scheme and net location then just
|
||||||
# use the path as the "next" url.
|
# use the path as the "next" url.
|
||||||
login_scheme, login_netloc = urlparse(login_url_as_str)[:2]
|
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
||||||
current_scheme, current_netloc = urlparse(path)[:2]
|
current_scheme, current_netloc = urlparse(path)[:2]
|
||||||
if ((not login_scheme or login_scheme == current_scheme) and
|
if ((not login_scheme or login_scheme == current_scheme) and
|
||||||
(not login_netloc or login_netloc == current_netloc)):
|
(not login_netloc or login_netloc == current_netloc)):
|
||||||
|
|
|
@ -25,7 +25,7 @@ class LoginRequiredTestCase(AuthViewsTestCase):
|
||||||
pass
|
pass
|
||||||
login_required(normal_view)
|
login_required(normal_view)
|
||||||
|
|
||||||
def testLoginRequired(self, view_url='/login_required/', login_url=settings.LOGIN_URL):
|
def testLoginRequired(self, view_url='/login_required/', login_url='/login/'):
|
||||||
"""
|
"""
|
||||||
Check that login_required works on a simple view wrapped in a
|
Check that login_required works on a simple view wrapped in a
|
||||||
login_required decorator.
|
login_required decorator.
|
||||||
|
|
|
@ -7,9 +7,9 @@ from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseRedirect, QueryDict
|
from django.http import HttpResponseRedirect, QueryDict
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.encoding import force_str
|
|
||||||
from django.utils.http import base36_to_int
|
from django.utils.http import base36_to_int
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.shortcuts import resolve_url
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
@ -38,16 +38,16 @@ def login(request, template_name='registration/login.html',
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = authentication_form(data=request.POST)
|
form = authentication_form(data=request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
netloc = urlparse(redirect_to)[1]
|
|
||||||
|
|
||||||
# Use default setting if redirect_to is empty
|
# Use default setting if redirect_to is empty
|
||||||
if not redirect_to:
|
if not redirect_to:
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||||
|
redirect_to = resolve_url(redirect_to)
|
||||||
|
|
||||||
|
netloc = urlparse(redirect_to)[1]
|
||||||
# Heavier security check -- don't allow redirection to a different
|
# Heavier security check -- don't allow redirection to a different
|
||||||
# host.
|
# host.
|
||||||
elif netloc and netloc != request.get_host():
|
if netloc and netloc != request.get_host():
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
# Okay, security checks complete. Log the user in.
|
# Okay, security checks complete. Log the user in.
|
||||||
auth_login(request, form.get_user())
|
auth_login(request, form.get_user())
|
||||||
|
@ -110,6 +110,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N
|
||||||
"""
|
"""
|
||||||
if not login_url:
|
if not login_url:
|
||||||
login_url = settings.LOGIN_URL
|
login_url = settings.LOGIN_URL
|
||||||
|
login_url = resolve_url(login_url)
|
||||||
return logout(request, login_url, current_app=current_app, extra_context=extra_context)
|
return logout(request, login_url, current_app=current_app, extra_context=extra_context)
|
||||||
|
|
||||||
def redirect_to_login(next, login_url=None,
|
def redirect_to_login(next, login_url=None,
|
||||||
|
@ -117,10 +118,9 @@ def redirect_to_login(next, login_url=None,
|
||||||
"""
|
"""
|
||||||
Redirects the user to the login page, passing the given 'next' page
|
Redirects the user to the login page, passing the given 'next' page
|
||||||
"""
|
"""
|
||||||
# urlparse chokes on lazy objects in Python 3
|
resolved_url = resolve_url(login_url or settings.LOGIN_URL)
|
||||||
login_url_as_str = force_str(login_url or settings.LOGIN_URL)
|
|
||||||
|
|
||||||
login_url_parts = list(urlparse(login_url_as_str))
|
login_url_parts = list(urlparse(resolved_url))
|
||||||
if redirect_field_name:
|
if redirect_field_name:
|
||||||
querystring = QueryDict(login_url_parts[4], mutable=True)
|
querystring = QueryDict(login_url_parts[4], mutable=True)
|
||||||
querystring[redirect_field_name] = next
|
querystring[redirect_field_name] = next
|
||||||
|
@ -229,7 +229,7 @@ def password_reset_complete(request,
|
||||||
template_name='registration/password_reset_complete.html',
|
template_name='registration/password_reset_complete.html',
|
||||||
current_app=None, extra_context=None):
|
current_app=None, extra_context=None):
|
||||||
context = {
|
context = {
|
||||||
'login_url': settings.LOGIN_URL
|
'login_url': resolve_url(settings.LOGIN_URL)
|
||||||
}
|
}
|
||||||
if extra_context is not None:
|
if extra_context is not None:
|
||||||
context.update(extra_context)
|
context.update(extra_context)
|
||||||
|
|
|
@ -66,23 +66,7 @@ def redirect(to, *args, **kwargs):
|
||||||
else:
|
else:
|
||||||
redirect_class = HttpResponseRedirect
|
redirect_class = HttpResponseRedirect
|
||||||
|
|
||||||
# If it's a model, use get_absolute_url()
|
return redirect_class(resolve_url(to, *args, **kwargs))
|
||||||
if hasattr(to, 'get_absolute_url'):
|
|
||||||
return redirect_class(to.get_absolute_url())
|
|
||||||
|
|
||||||
# Next try a reverse URL resolution.
|
|
||||||
try:
|
|
||||||
return redirect_class(urlresolvers.reverse(to, args=args, kwargs=kwargs))
|
|
||||||
except urlresolvers.NoReverseMatch:
|
|
||||||
# If this is a callable, re-raise.
|
|
||||||
if callable(to):
|
|
||||||
raise
|
|
||||||
# If this doesn't "feel" like a URL, re-raise.
|
|
||||||
if '/' not in to and '.' not in to:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Finally, fall back and assume it's a URL
|
|
||||||
return redirect_class(to)
|
|
||||||
|
|
||||||
def _get_queryset(klass):
|
def _get_queryset(klass):
|
||||||
"""
|
"""
|
||||||
|
@ -128,3 +112,34 @@ def get_list_or_404(klass, *args, **kwargs):
|
||||||
raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
|
raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
|
||||||
return obj_list
|
return obj_list
|
||||||
|
|
||||||
|
def resolve_url(to, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a URL appropriate for the arguments passed.
|
||||||
|
|
||||||
|
The arguments could be:
|
||||||
|
|
||||||
|
* A model: the model's `get_absolute_url()` function will be called.
|
||||||
|
|
||||||
|
* A view name, possibly with arguments: `urlresolvers.reverse()` will
|
||||||
|
be used to reverse-resolve the name.
|
||||||
|
|
||||||
|
* A URL, which will be returned as-is.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If it's a model, use get_absolute_url()
|
||||||
|
if hasattr(to, 'get_absolute_url'):
|
||||||
|
return to.get_absolute_url()
|
||||||
|
|
||||||
|
# Next try a reverse URL resolution.
|
||||||
|
try:
|
||||||
|
return urlresolvers.reverse(to, args=args, kwargs=kwargs)
|
||||||
|
except urlresolvers.NoReverseMatch:
|
||||||
|
# If this is a callable, re-raise.
|
||||||
|
if callable(to):
|
||||||
|
raise
|
||||||
|
# If this doesn't "feel" like a URL, re-raise.
|
||||||
|
if '/' not in to and '.' not in to:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Finally, fall back and assume it's a URL
|
||||||
|
return to
|
||||||
|
|
|
@ -1304,25 +1304,13 @@ The URL where requests are redirected after login when the
|
||||||
This is used by the :func:`~django.contrib.auth.decorators.login_required`
|
This is used by the :func:`~django.contrib.auth.decorators.login_required`
|
||||||
decorator, for example.
|
decorator, for example.
|
||||||
|
|
||||||
.. _`note on LOGIN_REDIRECT_URL setting`:
|
.. versionchanged:: 1.5
|
||||||
|
|
||||||
.. note::
|
This setting now also accepts view function names and
|
||||||
You can use :func:`~django.core.urlresolvers.reverse_lazy` to reference
|
:ref:`named URL patterns <naming-url-patterns>` which can be used to reduce
|
||||||
URLs by their name instead of providing a hardcoded value. Assuming a
|
configuration duplication since you no longer have to define the URL in two
|
||||||
``urls.py`` with an URLpattern named ``home``::
|
places (``settings`` and URLconf).
|
||||||
|
For backward compatibility reasons the default remains unchanged.
|
||||||
urlpatterns = patterns('',
|
|
||||||
url('^welcome/$', 'test_app.views.home', name='home'),
|
|
||||||
)
|
|
||||||
|
|
||||||
You can use :func:`~django.core.urlresolvers.reverse_lazy` like this::
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy('home')
|
|
||||||
|
|
||||||
This also works fine with localized URLs using
|
|
||||||
:func:`~django.conf.urls.i18n.i18n_patterns`.
|
|
||||||
|
|
||||||
.. setting:: LOGIN_URL
|
.. setting:: LOGIN_URL
|
||||||
|
|
||||||
|
@ -1334,8 +1322,13 @@ Default: ``'/accounts/login/'``
|
||||||
The URL where requests are redirected for login, especially when using the
|
The URL where requests are redirected for login, especially when using the
|
||||||
:func:`~django.contrib.auth.decorators.login_required` decorator.
|
:func:`~django.contrib.auth.decorators.login_required` decorator.
|
||||||
|
|
||||||
.. note::
|
.. versionchanged:: 1.5
|
||||||
See the `note on LOGIN_REDIRECT_URL setting`_
|
|
||||||
|
This setting now also accepts view function names and
|
||||||
|
:ref:`named URL patterns <naming-url-patterns>` which can be used to reduce
|
||||||
|
configuration duplication since you no longer have to define the URL in two
|
||||||
|
places (``settings`` and URLconf).
|
||||||
|
For backward compatibility reasons the default remains unchanged.
|
||||||
|
|
||||||
.. setting:: LOGOUT_URL
|
.. setting:: LOGOUT_URL
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,12 @@ Django 1.5 also includes several smaller improvements worth noting:
|
||||||
argument. By default the batch_size is unlimited except for SQLite where
|
argument. By default the batch_size is unlimited except for SQLite where
|
||||||
single batch is limited so that 999 parameters per query isn't exceeded.
|
single batch is limited so that 999 parameters per query isn't exceeded.
|
||||||
|
|
||||||
|
* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also
|
||||||
|
accept view function names and
|
||||||
|
:ref:`named URL patterns <naming-url-patterns>`. This allows you to reduce
|
||||||
|
configuration duplication. More information can be found in the
|
||||||
|
:func:`~django.contrib.auth.decorators.login_required` documentation.
|
||||||
|
|
||||||
Backwards incompatible changes in 1.5
|
Backwards incompatible changes in 1.5
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -947,6 +947,13 @@ The login_required decorator
|
||||||
|
|
||||||
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
|
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
|
||||||
|
|
||||||
|
.. versionchanged:: 1.5
|
||||||
|
|
||||||
|
As of version 1.5 :setting:`settings.LOGIN_URL <LOGIN_URL>` now also accepts
|
||||||
|
view function names and :ref:`named URL patterns <naming-url-patterns>`.
|
||||||
|
This allows you to freely remap your login view within your URLconf
|
||||||
|
without having to update the setting.
|
||||||
|
|
||||||
.. function:: views.login(request, [template_name, redirect_field_name, authentication_form])
|
.. function:: views.login(request, [template_name, redirect_field_name, authentication_form])
|
||||||
|
|
||||||
**URL name:** ``login``
|
**URL name:** ``login``
|
||||||
|
|
|
@ -17,7 +17,7 @@ CT = ContentType.objects.get_for_model
|
||||||
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',))
|
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',))
|
||||||
class CommentTestCase(TestCase):
|
class CommentTestCase(TestCase):
|
||||||
fixtures = ["comment_tests"]
|
fixtures = ["comment_tests"]
|
||||||
urls = 'django.contrib.comments.urls'
|
urls = 'regressiontests.comment_tests.urls_default'
|
||||||
|
|
||||||
def createSomeComments(self):
|
def createSomeComments(self):
|
||||||
# Two anonymous comments on two different objects
|
# Two anonymous comments on two different objects
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^', include('django.contrib.comments.urls')),
|
||||||
|
|
||||||
|
# Provide the auth system login and logout views
|
||||||
|
(r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}),
|
||||||
|
(r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
|
||||||
|
)
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""
|
||||||
|
Regression tests for the resolve_url function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class UnimportantThing(models.Model):
|
||||||
|
importance = models.IntegerField()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return '/importance/%d/' % (self.importance,)
|
|
@ -0,0 +1,68 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.core.urlresolvers import NoReverseMatch
|
||||||
|
from django.contrib.auth.views import logout
|
||||||
|
from django.utils.unittest import TestCase
|
||||||
|
from django.shortcuts import resolve_url
|
||||||
|
|
||||||
|
from .models import UnimportantThing
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveUrlTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the ``resolve_url`` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_url_path(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a URL path to ``resolve_url`` will result in the
|
||||||
|
same url.
|
||||||
|
"""
|
||||||
|
self.assertEqual('/something/', resolve_url('/something/'))
|
||||||
|
|
||||||
|
def test_full_url(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a full URL to ``resolve_url`` will result in the
|
||||||
|
same url.
|
||||||
|
"""
|
||||||
|
url = 'http://example.com/'
|
||||||
|
self.assertEqual(url, resolve_url(url))
|
||||||
|
|
||||||
|
def test_model(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a model to ``resolve_url`` will result in
|
||||||
|
``get_absolute_url`` being called on that model instance.
|
||||||
|
"""
|
||||||
|
m = UnimportantThing(importance=1)
|
||||||
|
self.assertEqual(m.get_absolute_url(), resolve_url(m))
|
||||||
|
|
||||||
|
def test_view_function(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a view name to ``resolve_url`` will result in the
|
||||||
|
URL path mapping to that view name.
|
||||||
|
"""
|
||||||
|
resolved_url = resolve_url(logout)
|
||||||
|
self.assertEqual('/accounts/logout/', resolved_url)
|
||||||
|
|
||||||
|
def test_valid_view_name(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a view function to ``resolve_url`` will result in
|
||||||
|
the URL path mapping to that view.
|
||||||
|
"""
|
||||||
|
resolved_url = resolve_url('django.contrib.auth.views.logout')
|
||||||
|
self.assertEqual('/accounts/logout/', resolved_url)
|
||||||
|
|
||||||
|
def test_domain(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a domain to ``resolve_url`` returns the same domain.
|
||||||
|
"""
|
||||||
|
self.assertEqual(resolve_url('example.com'), 'example.com')
|
||||||
|
|
||||||
|
def test_non_view_callable_raises_no_reverse_match(self):
|
||||||
|
"""
|
||||||
|
Tests that passing a non-view callable into ``resolve_url`` raises a
|
||||||
|
``NoReverseMatch`` exception.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(NoReverseMatch):
|
||||||
|
resolve_url(lambda: 'asdf')
|
||||||
|
|
|
@ -92,7 +92,7 @@ def setup(verbosity, test_labels):
|
||||||
settings.TEMPLATE_DIRS = (os.path.join(RUNTESTS_DIR, TEST_TEMPLATE_DIR),)
|
settings.TEMPLATE_DIRS = (os.path.join(RUNTESTS_DIR, TEST_TEMPLATE_DIR),)
|
||||||
settings.USE_I18N = True
|
settings.USE_I18N = True
|
||||||
settings.LANGUAGE_CODE = 'en'
|
settings.LANGUAGE_CODE = 'en'
|
||||||
settings.LOGIN_URL = '/accounts/login/'
|
settings.LOGIN_URL = 'django.contrib.auth.views.login'
|
||||||
settings.MIDDLEWARE_CLASSES = (
|
settings.MIDDLEWARE_CLASSES = (
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
|
Loading…
Reference in New Issue