Przeglądaj źródła

Fixed #24097 -- Prevented AttributeError in redirect_to_login

Thanks Peter Schmidt for the report and the initial patch.
Thanks to ​Oktay Sancak for writing the original failing test and
Alvin Savoy for supporting contributing back to the community.
Claude Paroz 10 lat temu
rodzic
commit
d7bc37d611

+ 1 - 4
django/contrib/auth/decorators.py

@@ -3,7 +3,6 @@ from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
 from django.core.exceptions import PermissionDenied
 from django.utils.decorators import available_attrs
-from django.utils.encoding import force_str
 from django.utils.six.moves.urllib.parse import urlparse
 from django.shortcuts import resolve_url
 
@@ -21,9 +20,7 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
             if test_func(request.user):
                 return view_func(request, *args, **kwargs)
             path = request.build_absolute_uri()
-            # urlparse chokes on lazy objects in Python 3, force to str
-            resolved_login_url = force_str(
-                resolve_url(login_url or settings.LOGIN_URL))
+            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
             # If the login url is the same scheme and net location then just
             # use the path as the "next" url.
             login_scheme, login_netloc = urlparse(resolved_login_url)[:2]

+ 24 - 2
django/contrib/auth/tests/test_views.py

@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
 from importlib import import_module
 import itertools
 import re
@@ -10,9 +13,9 @@ from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
 from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
     SetPasswordForm)
 from django.contrib.auth.models import User
-from django.contrib.auth.views import login as login_view
+from django.contrib.auth.views import login as login_view, redirect_to_login
 from django.core import mail
-from django.core.urlresolvers import reverse, NoReverseMatch
+from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
 from django.http import QueryDict, HttpRequest
 from django.utils.deprecation import RemovedInDjango20Warning
 from django.utils.encoding import force_text
@@ -648,6 +651,10 @@ class LoginURLSettings(AuthViewsTestCase):
         expected = 'http://remote.example.com/login/?next=%s' % quoted_next
         self.assertLoginURLEquals(expected)
 
+    @override_settings(LOGIN_URL=reverse_lazy('login'))
+    def test_lazy_login_url(self):
+        self.assertLoginURLEquals('/login/?next=/login_required/')
+
 
 @skipIfCustomUser
 class LoginRedirectUrlTest(AuthViewsTestCase):
@@ -673,6 +680,21 @@ class LoginRedirectUrlTest(AuthViewsTestCase):
         self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/')
 
 
+class RedirectToLoginTests(AuthViewsTestCase):
+    """Tests for the redirect_to_login view"""
+    @override_settings(LOGIN_URL=reverse_lazy('login'))
+    def test_redirect_to_login_with_lazy(self):
+        login_redirect_response = redirect_to_login(next='/else/where/')
+        expected = '/login/?next=/else/where/'
+        self.assertEqual(expected, login_redirect_response.url)
+
+    @override_settings(LOGIN_URL=reverse_lazy('login'))
+    def test_redirect_to_login_with_lazy_and_unicode(self):
+        login_redirect_response = redirect_to_login(next='/else/where/झ/')
+        expected = '/login/?next=/else/where/%E0%A4%9D/'
+        self.assertEqual(expected, login_redirect_response.url)
+
+
 @skipIfCustomUser
 class LogoutTest(AuthViewsTestCase):
 

+ 7 - 0
django/shortcuts.py

@@ -18,6 +18,8 @@ from django.db.models.query import QuerySet
 from django.core import urlresolvers
 from django.utils import six
 from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.encoding import force_text
+from django.utils.functional import Promise
 
 
 def render_to_response(template_name, context=None,
@@ -182,6 +184,11 @@ def resolve_url(to, *args, **kwargs):
     if hasattr(to, 'get_absolute_url'):
         return to.get_absolute_url()
 
+    if isinstance(to, Promise):
+        # Expand the lazy instance, as it can cause issues when it is passed
+        # further to some Python functions like urlparse.
+        to = force_text(to)
+
     if isinstance(to, six.string_types):
         # Handle relative URLs
         if any(to.startswith(path) for path in ('./', '../')):

+ 4 - 0
docs/releases/1.7.3.txt

@@ -20,3 +20,7 @@ Bugfixes
 
 * Fixed a crash in the CSRF middleware when handling non-ASCII referer header
   (:ticket:`23815`).
+
+* Fixed a crash in the ``django.contrib.auth.redirect_to_login`` view when
+  passing a :func:`~django.core.urlresolvers.reverse_lazy` result on Python 3
+  (:ticket:`24097`).

+ 11 - 1
tests/resolve_url/tests.py

@@ -1,10 +1,11 @@
 from __future__ import unicode_literals
 
-from django.core.urlresolvers import NoReverseMatch
+from django.core.urlresolvers import NoReverseMatch, reverse_lazy
 from django.contrib.auth.views import logout
 from django.shortcuts import resolve_url
 from django.test import TestCase, ignore_warnings, override_settings
 from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils import six
 
 from .models import UnimportantThing
 
@@ -56,6 +57,15 @@ class ResolveUrlTests(TestCase):
         resolved_url = resolve_url(logout)
         self.assertEqual('/accounts/logout/', resolved_url)
 
+    def test_lazy_reverse(self):
+        """
+        Tests that passing the result of reverse_lazy is resolved to a real URL
+        string.
+        """
+        resolved_url = resolve_url(reverse_lazy('logout'))
+        self.assertIsInstance(resolved_url, six.text_type)
+        self.assertEqual('/accounts/logout/', resolved_url)
+
     @ignore_warnings(category=RemovedInDjango20Warning)
     def test_valid_view_name(self):
         """

+ 1 - 1
tests/resolve_url/urls.py

@@ -2,5 +2,5 @@ from django.conf.urls import url
 from django.contrib.auth import views
 
 urlpatterns = [
-    url(r'^accounts/logout/$', views.logout),
+    url(r'^accounts/logout/$', views.logout, name='logout'),
 ]