Commit d7bc37d6 authored by Claude Paroz's avatar Claude Paroz
Browse files

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.
parent f5c3a8bf
Loading
Loading
Loading
Loading
+1 −4
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
# -*- 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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
@@ -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
Original line number Diff line number Diff line
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):
        """
Loading