Browse Source

Fixed #25847 -- Made User.is_(anonymous|authenticated) properties.

Jeremy Lainé 9 years ago
parent
commit
c1aec0feda

+ 1 - 0
AUTHORS

@@ -345,6 +345,7 @@ answer newbie questions, and generally made Django that much better:
     Jérémie Blaser <blaserje@gmail.com>
     Jeremy Carbaugh <jcarbaugh@gmail.com>
     Jeremy Dunck <jdunck@gmail.com>
+    Jeremy Lainé <jeremy.laine@m4x.org>
     Jesse Young <adunar@gmail.com>
     jhenry <jhenry@theonion.com>
     Jim Dalton <jim.dalton@gmail.com>

+ 1 - 1
django/contrib/auth/__init__.py

@@ -138,7 +138,7 @@ def logout(request):
     # Dispatch the signal before the user is logged out so the receivers have a
     # chance to find out *who* logged out.
     user = getattr(request, 'user', None)
-    if hasattr(user, 'is_authenticated') and not user.is_authenticated():
+    if hasattr(user, 'is_authenticated') and not user.is_authenticated:
         user = None
     user_logged_out.send(sender=user.__class__, request=request, user=user)
 

+ 2 - 2
django/contrib/auth/backends.py

@@ -45,7 +45,7 @@ class ModelBackend(object):
         be either "group" or "user" to return permissions from
         `_get_group_permissions` or `_get_user_permissions` respectively.
         """
-        if not user_obj.is_active or user_obj.is_anonymous() or obj is not None:
+        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
             return set()
 
         perm_cache_name = '_%s_perm_cache' % from_name
@@ -73,7 +73,7 @@ class ModelBackend(object):
         return self._get_permissions(user_obj, obj, 'group')
 
     def get_all_permissions(self, user_obj, obj=None):
-        if not user_obj.is_active or user_obj.is_anonymous() or obj is not None:
+        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
             return set()
         if not hasattr(user_obj, '_perm_cache'):
             user_obj._perm_cache = self.get_user_permissions(user_obj)

+ 5 - 2
django/contrib/auth/base_user.py

@@ -10,6 +10,7 @@ from django.contrib.auth.hashers import (
 )
 from django.db import models
 from django.utils.crypto import get_random_string, salted_hmac
+from django.utils.deprecation import CallableFalse, CallableTrue
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 
@@ -79,19 +80,21 @@ class AbstractBaseUser(models.Model):
     def natural_key(self):
         return (self.get_username(),)
 
+    @property
     def is_anonymous(self):
         """
         Always return False. This is a way of comparing User objects to
         anonymous users.
         """
-        return False
+        return CallableFalse
 
+    @property
     def is_authenticated(self):
         """
         Always return True. This is a way to tell if the user has been
         authenticated in templates.
         """
-        return True
+        return CallableTrue
 
     def set_password(self, raw_password):
         self.password = make_password(raw_password)

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

@@ -43,7 +43,7 @@ def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login
     to the log-in page if necessary.
     """
     actual_decorator = user_passes_test(
-        lambda u: u.is_authenticated(),
+        lambda u: u.is_authenticated,
         login_url=login_url,
         redirect_field_name=redirect_field_name
     )

+ 2 - 2
django/contrib/auth/middleware.py

@@ -70,13 +70,13 @@ class RemoteUserMiddleware(object):
             # If specified header doesn't exist then remove any existing
             # authenticated remote-user, or return (leaving request.user set to
             # AnonymousUser by the AuthenticationMiddleware).
-            if self.force_logout_if_no_header and request.user.is_authenticated():
+            if self.force_logout_if_no_header and request.user.is_authenticated:
                 self._remove_invalid_user(request)
             return
         # If the user is already authenticated and that user is the user we are
         # getting passed in the headers, then the correct user is already
         # persisted in the session and we don't need to continue.
-        if request.user.is_authenticated():
+        if request.user.is_authenticated:
             if request.user.get_username() == self.clean_username(username, request):
                 return
             else:

+ 1 - 1
django/contrib/auth/mixins.py

@@ -51,7 +51,7 @@ class LoginRequiredMixin(AccessMixin):
     CBV mixin which verifies that the current user is authenticated.
     """
     def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_authenticated():
+        if not request.user.is_authenticated:
             return self.handle_no_permission()
         return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
 

+ 5 - 2
django/contrib/auth/models.py

@@ -10,6 +10,7 @@ from django.core.mail import send_mail
 from django.db import models
 from django.db.models.manager import EmptyManager
 from django.utils import six, timezone
+from django.utils.deprecation import CallableFalse, CallableTrue
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 
@@ -438,11 +439,13 @@ class AnonymousUser(object):
     def has_module_perms(self, module):
         return _user_has_module_perms(self, module)
 
+    @property
     def is_anonymous(self):
-        return True
+        return CallableTrue
 
+    @property
     def is_authenticated(self):
-        return False
+        return CallableFalse
 
     def get_username(self):
         return self.username

+ 1 - 1
django/contrib/auth/views.py

@@ -68,7 +68,7 @@ def login(request, template_name='registration/login.html',
     """
     redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name, ''))
 
-    if redirect_authenticated_user and request.user.is_authenticated():
+    if redirect_authenticated_user and request.user.is_authenticated:
         redirect_to = _get_login_redirect_url(request, redirect_to)
         if redirect_to == request.path:
             raise ValueError(

+ 1 - 1
django/contrib/flatpages/templatetags/flatpages.py

@@ -33,7 +33,7 @@ class FlatpageNode(template.Node):
         # was provided, filter the list to only public flatpages.
         if self.user:
             user = self.user.resolve(context)
-            if not user.is_authenticated():
+            if not user.is_authenticated:
                 flatpages = flatpages.filter(registration_required=False)
         else:
             flatpages = flatpages.filter(registration_required=False)

+ 1 - 1
django/contrib/flatpages/views.py

@@ -52,7 +52,7 @@ def render_flatpage(request, f):
     """
     # If registration is required for accessing this page, and the user isn't
     # logged in, redirect to the login page.
-    if f.registration_required and not request.user.is_authenticated():
+    if f.registration_required and not request.user.is_authenticated:
         from django.contrib.auth.views import redirect_to_login
         return redirect_to_login(request.path)
     if f.template_name:

+ 30 - 0
django/utils/deprecation.py

@@ -79,3 +79,33 @@ class DeprecationInstanceCheck(type):
             self.deprecation_warning, 2
         )
         return super(DeprecationInstanceCheck, self).__instancecheck__(instance)
+
+
+class CallableBool:
+    """
+    An boolean-like object that is also callable for backwards compatibility.
+    """
+    do_not_call_in_templates = True
+
+    def __init__(self, value):
+        self.value = value
+
+    def __bool__(self):
+        return self.value
+
+    def __call__(self):
+        warnings.warn(
+            "Using user.is_authenticated() and user.is_anonymous() as a method "
+            "is deprecated. Remove the parentheses to use it as an attribute.",
+            RemovedInDjango20Warning, stacklevel=2
+        )
+        return self.value
+
+    def __nonzero__(self):  # Python 2 compatibility
+        return self.value
+
+    def __repr__(self):
+        return 'CallableBool(%r)' % self.value
+
+CallableFalse = CallableBool(False)
+CallableTrue = CallableBool(True)

+ 1 - 1
docs/howto/error-reporting.txt

@@ -256,7 +256,7 @@ given view by setting the ``HttpRequest``’s ``exception_reporter_filter``
 attribute::
 
     def my_view(request):
-        if request.user.is_authenticated():
+        if request.user.is_authenticated:
             request.exception_reporter_filter = CustomExceptionReporterFilter()
         ...
 

+ 3 - 0
docs/internals/deprecation.txt

@@ -147,6 +147,9 @@ details on these changes.
 * The shim for supporting custom related manager classes without a
   ``_apply_rel_filters()`` method will be removed.
 
+* Using ``User.is_authenticated()`` and ``User.is_anonymous()`` as methods
+  will no longer be supported.
+
 .. _deprecation-removed-in-1.10:
 
 1.10

+ 43 - 30
docs/ref/contrib/auth.txt

@@ -111,6 +111,41 @@ Fields
         A datetime designating when the account was created. Is set to the
         current date/time by default when the account is created.
 
+Attributes
+----------
+
+.. class:: models.User
+
+    .. attribute:: is_authenticated
+
+        Read-only attribute which is always ``True`` (as opposed to
+        ``AnonymousUser.is_authenticated`` which is always ``False``). This is
+        a way to tell if the user has been authenticated. This does not imply
+        any permissions and doesn't check if the user is active or has a valid
+        session. Even though normally you will check this attribute on
+        ``request.user`` to find out whether it has been populated by the
+        :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`
+        (representing the currently logged-in user), you should know this
+        attribute is ``True`` for any :class:`~models.User` instance.
+
+        .. versionchanged:: 1.10
+
+            In older versions, this was a method. Backwards-compatibility
+            support for using it as a method will be removed in Django 2.0.
+
+    .. attribute:: is_anonymous
+
+        Read-only attribute which is always ``False``. This is a way of
+        differentiating :class:`~models.User` and :class:`~models.AnonymousUser`
+        objects. Generally, you should prefer using
+        :attr:`~django.contrib.auth.models.User.is_authenticated` to this
+        attribute.
+
+        .. versionchanged:: 1.10
+
+            In older versions, this was a method. Backwards-compatibility
+            support for using it as a method will be removed in Django 2.0.
+
 Methods
 -------
 
@@ -119,31 +154,9 @@ Methods
     .. method:: get_username()
 
         Returns the username for the user. Since the User model can be swapped
-        out, you should use  this method instead of referencing the username
+        out, you should use this method instead of referencing the username
         attribute directly.
 
-    .. method:: is_anonymous()
-
-        Always returns ``False``. This is a way of differentiating
-        :class:`~django.contrib.auth.models.User` and
-        :class:`~django.contrib.auth.models.AnonymousUser` objects.
-        Generally, you should prefer using
-        :meth:`~django.contrib.auth.models.User.is_authenticated()` to this
-        method.
-
-    .. method:: is_authenticated()
-
-        Always returns ``True`` (as opposed to
-        ``AnonymousUser.is_authenticated()`` which always returns ``False``).
-        This is a way to tell if the user has been authenticated. This does not
-        imply any permissions, and doesn't check if the user is active or has
-        a valid session. Even though normally you will call this method on
-        ``request.user`` to find out whether it has been populated by the
-        :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`
-        (representing the currently logged-in user), you should know this method
-        returns ``True`` for any :class:`~django.contrib.auth.models.User`
-        instance.
-
     .. method:: get_full_name()
 
         Returns the :attr:`~django.contrib.auth.models.User.first_name` plus
@@ -287,6 +300,10 @@ Manager methods
       string.
     * :meth:`~django.contrib.auth.models.User.get_username()` always returns
       the empty string.
+    * :attr:`~django.contrib.auth.models.User.is_anonymous` is ``True``
+      instead of ``False``.
+    * :attr:`~django.contrib.auth.models.User.is_authenticated` is
+      ``False`` instead of ``True``.
     * :attr:`~django.contrib.auth.models.User.is_staff` and
       :attr:`~django.contrib.auth.models.User.is_superuser` are always
       ``False``.
@@ -294,10 +311,6 @@ Manager methods
     * :attr:`~django.contrib.auth.models.User.groups` and
       :attr:`~django.contrib.auth.models.User.user_permissions` are always
       empty.
-    * :meth:`~django.contrib.auth.models.User.is_anonymous()` returns ``True``
-      instead of ``False``.
-    * :meth:`~django.contrib.auth.models.User.is_authenticated()` returns
-      ``False`` instead of ``True``.
     * :meth:`~django.contrib.auth.models.User.set_password()`,
       :meth:`~django.contrib.auth.models.User.check_password()`,
       :meth:`~django.db.models.Model.save` and
@@ -471,21 +484,21 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
 
         Returns the set of permission strings the ``user_obj`` has from their
         own user permissions. Returns an empty set if
-        :meth:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
+        :attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
         :attr:`~django.contrib.auth.models.CustomUser.is_active` is ``False``.
 
     .. method:: get_group_permissions(user_obj, obj=None)
 
         Returns the set of permission strings the ``user_obj`` has from the
         permissions of the groups they belong. Returns an empty set if
-        :meth:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
+        :attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
         :attr:`~django.contrib.auth.models.CustomUser.is_active`  is ``False``.
 
     .. method:: get_all_permissions(user_obj, obj=None)
 
         Returns the set of permission strings the ``user_obj`` has, including both
         user permissions and group permissions. Returns an empty set if
-        :meth:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
+        :attr:`~django.contrib.auth.models.AbstractBaseUser.is_anonymous` or
         :attr:`~django.contrib.auth.models.CustomUser.is_active` is ``False``.
 
     .. method:: has_perm(user_obj, perm, obj=None)

+ 2 - 2
docs/ref/request-response.txt

@@ -233,9 +233,9 @@ middleware class is listed in :setting:`MIDDLEWARE_CLASSES`.
     logged-in user. If the user isn't currently logged in, ``user`` will be set
     to an instance of :class:`~django.contrib.auth.models.AnonymousUser`. You
     can tell them apart with
-    :meth:`~django.contrib.auth.models.User.is_authenticated`, like so::
+    :attr:`~django.contrib.auth.models.User.is_authenticated`, like so::
 
-        if request.user.is_authenticated():
+        if request.user.is_authenticated:
             ... # Do something for logged-in users.
         else:
             ... # Do something for anonymous users.

+ 35 - 0
docs/releases/1.10.txt

@@ -730,6 +730,10 @@ Miscellaneous
 * Middleware classes are now initialized when the server starts rather than
   during the first request.
 
+* If you override ``is_authenticated()`` or ``is_anonymous()`` in a custom user
+  model, you must convert them to attributes or properties as described in
+  :ref:`the deprecation note <user-is-auth-anon-deprecation>`.
+
 .. _deprecated-features-1.10:
 
 Features deprecated in 1.10
@@ -857,6 +861,37 @@ features, is deprecated. Replace it with a custom lookup::
     models.CharField.register_lookup(Search)
     models.TextField.register_lookup(Search)
 
+.. _user-is-auth-anon-deprecation:
+
+Using ``User.is_authenticated()`` and ``User.is_anonymous()`` as methods
+------------------------------------------------------------------------
+
+The ``is_authenticated()`` and ``is_anonymous()`` methods of
+:class:`~django.contrib.auth.models.AbstractBaseUser` and
+:class:`~django.contrib.auth.models.AnonymousUser` classes are now
+properties. They will still work as methods until Django 2.0, but all usage
+in Django now uses attribute access.
+
+For example, if you use
+:class:`~django.contrib.auth.middleware.AuthenticationMiddleware` and want
+to know whether the user is currently logged-in you would use::
+
+    if request.user.is_authenticated:
+        ... # Do something for logged-in users.
+    else:
+        ... # Do something for anonymous users.
+
+instead of ``request.user.is_authenticated()``.
+
+This change avoids accidental information leakage if you forget to call the
+method, e.g.::
+
+    if request.user.is_authenticated:
+        return sensitive_information
+
+If you override these methods in a custom user model, you must change them to
+properties or attributes.
+
 Custom manager classes available through ``prefetch_related`` must define a ``_apply_rel_filters()`` method
 -----------------------------------------------------------------------------------------------------------
 

+ 26 - 12
docs/topics/auth/customizing.txt

@@ -603,7 +603,7 @@ password resets. You must then provide some key implementation details:
         raised a deprecation warning in older versions and is no longer
         supported in Django 1.9).
 
-The following methods are available on any subclass of
+The following attributes and methods are available on any subclass of
 :class:`~django.contrib.auth.models.AbstractBaseUser`:
 
 .. class:: models.AbstractBaseUser
@@ -612,20 +612,34 @@ The following methods are available on any subclass of
 
         Returns the value of the field nominated by ``USERNAME_FIELD``.
 
-    .. method:: models.AbstractBaseUser.is_anonymous()
+    .. attribute:: models.AbstractBaseUser.is_authenticated
 
-        Always returns ``False``. This is a way of differentiating
-        from  :class:`~django.contrib.auth.models.AnonymousUser` objects.
-        Generally, you should prefer using
-        :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this
-        method.
+        Read-only attribute which is always ``True`` (as opposed to
+        ``AnonymousUser.is_authenticated`` which is always ``False``).
+        This is a way to tell if the user has been authenticated. This does not
+        imply any permissions and doesn't check if the user is active or has
+        a valid session. Even though normally you will check this attribute on
+        ``request.user`` to find out whether it has been populated by the
+        :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`
+        (representing the currently logged-in user), you should know this
+        attribute is ``True`` for any :class:`~models.User` instance.
 
-    .. method:: models.AbstractBaseUser.is_authenticated()
+        .. versionchanged:: 1.10
 
-        Always returns ``True``. This is a way to tell if the user has been
-        authenticated. This does not imply any permissions, and doesn't check
-        if the user is active - it only indicates that the user has provided a
-        valid username and password.
+            In older versions, this was a method. Backwards-compatibility
+            support for using it as a method will be removed in Django 2.0.
+
+    .. attribute:: models.AbstractBaseUser.is_anonymous
+
+        Read-only attribute which is always ``False``. This is a way of
+        differentiating :class:`~models.User` and :class:`~models.AnonymousUser`
+        objects. Generally, you should prefer using
+        :attr:`~models.User.is_authenticated` to this attribute.
+
+        .. versionchanged:: 1.10
+
+            In older versions, this was a method. Backwards-compatibility
+            support for using it as a method will be removed in Django 2.0.
 
     .. method:: models.AbstractBaseUser.set_password(raw_password)
 

+ 6 - 6
docs/topics/auth/default.txt

@@ -306,9 +306,9 @@ of :class:`~django.contrib.auth.models.AnonymousUser`, otherwise it will be an
 instance of :class:`~django.contrib.auth.models.User`.
 
 You can tell them apart with
-:meth:`~django.contrib.auth.models.User.is_authenticated()`, like so::
+:attr:`~django.contrib.auth.models.User.is_authenticated`, like so::
 
-    if request.user.is_authenticated():
+    if request.user.is_authenticated:
         # Do something for authenticated users.
         ...
     else:
@@ -421,15 +421,15 @@ The raw way
 ~~~~~~~~~~~
 
 The simple, raw way to limit access to pages is to check
-:meth:`request.user.is_authenticated()
-<django.contrib.auth.models.User.is_authenticated()>` and either redirect to a
+:attr:`request.user.is_authenticated
+<django.contrib.auth.models.User.is_authenticated>` and either redirect to a
 login page::
 
     from django.conf import settings
     from django.shortcuts import redirect
 
     def my_view(request):
-        if not request.user.is_authenticated():
+        if not request.user.is_authenticated:
             return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
         # ...
 
@@ -438,7 +438,7 @@ login page::
     from django.shortcuts import render
 
     def my_view(request):
-        if not request.user.is_authenticated():
+        if not request.user.is_authenticated:
             return render(request, 'myapp/login_error.html')
         # ...
 

+ 1 - 1
docs/topics/cache.txt

@@ -1170,7 +1170,7 @@ decorator)::
 
     @vary_on_cookie
     def list_blog_entries_view(request):
-        if request.user.is_anonymous():
+        if request.user.is_anonymous:
             response = render_only_public_entries()
             patch_cache_control(response, public=True)
         else:

+ 3 - 3
docs/topics/class-based-views/mixins.txt

@@ -235,7 +235,7 @@ We'll demonstrate this with the ``Author`` model we used in the
         model = Author
 
         def post(self, request, *args, **kwargs):
-            if not request.user.is_authenticated():
+            if not request.user.is_authenticated:
                 return HttpResponseForbidden()
 
             # Look up the author we're interested in.
@@ -466,7 +466,7 @@ Our new ``AuthorDetail`` looks like this::
             return context
 
         def post(self, request, *args, **kwargs):
-            if not request.user.is_authenticated():
+            if not request.user.is_authenticated:
                 return HttpResponseForbidden()
             self.object = self.get_object()
             form = self.get_form()
@@ -552,7 +552,7 @@ template as ``AuthorDisplay`` is using on ``GET``::
         model = Author
 
         def post(self, request, *args, **kwargs):
-            if not request.user.is_authenticated():
+            if not request.user.is_authenticated:
                 return HttpResponseForbidden()
             self.object = self.get_object()
             return super(AuthorInterest, self).post(request, *args, **kwargs)

+ 9 - 10
tests/auth_tests/test_auth_backends.py

@@ -12,7 +12,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.http import HttpRequest
 from django.test import (
-    SimpleTestCase, TestCase, modify_settings, override_settings,
+    SimpleTestCase, TestCase, mock, modify_settings, override_settings,
 )
 
 from .models import (
@@ -142,11 +142,10 @@ class BaseModelBackendTest(object):
         self.assertEqual(backend.get_user_permissions(user), {'auth.test_user', 'auth.test_group'})
         self.assertEqual(backend.get_group_permissions(user), {'auth.test_group'})
 
-        user.is_anonymous = lambda: True
-
-        self.assertEqual(backend.get_all_permissions(user), set())
-        self.assertEqual(backend.get_user_permissions(user), set())
-        self.assertEqual(backend.get_group_permissions(user), set())
+        with mock.patch.object(self.UserModel, 'is_anonymous', True):
+            self.assertEqual(backend.get_all_permissions(user), set())
+            self.assertEqual(backend.get_user_permissions(user), set())
+            self.assertEqual(backend.get_group_permissions(user), set())
 
     def test_inactive_has_no_permissions(self):
         """
@@ -334,14 +333,14 @@ class SimpleRowlevelBackend(object):
         if isinstance(obj, TestObj):
             if user.username == 'test2':
                 return True
-            elif user.is_anonymous() and perm == 'anon':
+            elif user.is_anonymous and perm == 'anon':
                 return True
             elif not user.is_active and perm == 'inactive':
                 return True
         return False
 
     def has_module_perms(self, user, app_label):
-        if not user.is_anonymous() and not user.is_active:
+        if not user.is_anonymous and not user.is_active:
             return False
         return app_label == "app1"
 
@@ -352,7 +351,7 @@ class SimpleRowlevelBackend(object):
         if not isinstance(obj, TestObj):
             return ['none']
 
-        if user.is_anonymous():
+        if user.is_anonymous:
             return ['anon']
         if user.username == 'test2':
             return ['simple', 'advanced']
@@ -578,7 +577,7 @@ class ChangedBackendSettingsTest(TestCase):
             # Assert that the user retrieval is successful and the user is
             # anonymous as the backend is not longer available.
             self.assertIsNotNone(user)
-            self.assertTrue(user.is_anonymous())
+            self.assertTrue(user.is_anonymous)
 
 
 class TypeErrorBackend(object):

+ 46 - 2
tests/auth_tests/test_basic.py

@@ -1,5 +1,7 @@
 from __future__ import unicode_literals
 
+import warnings
+
 from django.apps import apps
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AnonymousUser, User
@@ -44,7 +46,8 @@ class BasicTestCase(TestCase):
         self.assertEqual(u.get_username(), 'testuser')
 
         # Check authentication/permissions
-        self.assertTrue(u.is_authenticated())
+        self.assertFalse(u.is_anonymous)
+        self.assertTrue(u.is_authenticated)
         self.assertFalse(u.is_staff)
         self.assertTrue(u.is_active)
         self.assertFalse(u.is_superuser)
@@ -53,6 +56,26 @@ class BasicTestCase(TestCase):
         u2 = User.objects.create_user('testuser2', 'test2@example.com')
         self.assertFalse(u2.has_usable_password())
 
+    def test_is_anonymous_authenticated_method_deprecation(self):
+        deprecation_message = (
+            'Using user.is_authenticated() and user.is_anonymous() as a '
+            'method is deprecated. Remove the parentheses to use it as an '
+            'attribute.'
+        )
+        u = User.objects.create_user('testuser', 'test@example.com', 'testpw')
+        # Backwards-compatibility callables
+        with warnings.catch_warnings(record=True) as warns:
+            warnings.simplefilter('always')
+            self.assertFalse(u.is_anonymous())
+            self.assertEqual(len(warns), 1)
+            self.assertEqual(str(warns[0].message), deprecation_message)
+
+        with warnings.catch_warnings(record=True) as warns:
+            warnings.simplefilter('always')
+            self.assertTrue(u.is_authenticated())
+            self.assertEqual(len(warns), 1)
+            self.assertEqual(str(warns[0].message), deprecation_message)
+
     def test_user_no_email(self):
         "Check that users can be created without an email"
         u = User.objects.create_user('testuser1')
@@ -70,13 +93,34 @@ class BasicTestCase(TestCase):
         self.assertEqual(a.pk, None)
         self.assertEqual(a.username, '')
         self.assertEqual(a.get_username(), '')
-        self.assertFalse(a.is_authenticated())
+        self.assertTrue(a.is_anonymous)
+        self.assertFalse(a.is_authenticated)
         self.assertFalse(a.is_staff)
         self.assertFalse(a.is_active)
         self.assertFalse(a.is_superuser)
         self.assertEqual(a.groups.all().count(), 0)
         self.assertEqual(a.user_permissions.all().count(), 0)
 
+    def test_anonymous_user_is_anonymous_authenticated_method_deprecation(self):
+        a = AnonymousUser()
+        deprecation_message = (
+            'Using user.is_authenticated() and user.is_anonymous() as a '
+            'method is deprecated. Remove the parentheses to use it as an '
+            'attribute.'
+        )
+        # Backwards-compatibility callables
+        with warnings.catch_warnings(record=True) as warns:
+            warnings.simplefilter('always')  # prevent warnings from appearing as errors
+            self.assertTrue(a.is_anonymous())
+            self.assertEqual(len(warns), 1)
+            self.assertEqual(str(warns[0].message), deprecation_message)
+
+        with warnings.catch_warnings(record=True) as warns:
+            warnings.simplefilter('always')  # prevent warnings from appearing as errors
+            self.assertFalse(a.is_authenticated())
+            self.assertEqual(len(warns), 1)
+            self.assertEqual(str(warns[0].message), deprecation_message)
+
     def test_superuser(self):
         "Check the creation and properties of a superuser"
         super = User.objects.create_superuser('super', 'super@example.com', 'super')

+ 2 - 2
tests/auth_tests/test_middleware.py

@@ -16,7 +16,7 @@ class TestAuthenticationMiddleware(TestCase):
         self.request.session = self.client.session
         self.middleware.process_request(self.request)
         self.assertIsNotNone(self.request.user)
-        self.assertFalse(self.request.user.is_anonymous())
+        self.assertFalse(self.request.user.is_anonymous)
 
     def test_changed_password_invalidates_session(self):
         # After password change, user should be anonymous
@@ -24,6 +24,6 @@ class TestAuthenticationMiddleware(TestCase):
         self.user.save()
         self.middleware.process_request(self.request)
         self.assertIsNotNone(self.request.user)
-        self.assertTrue(self.request.user.is_anonymous())
+        self.assertTrue(self.request.user.is_anonymous)
         # session should be flushed
         self.assertIsNone(self.request.session.session_key)

+ 2 - 2
tests/auth_tests/test_mixins.py

@@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
 from django.contrib.auth.models import AnonymousUser
 from django.core.exceptions import PermissionDenied
 from django.http import HttpResponse
-from django.test import RequestFactory, TestCase
+from django.test import RequestFactory, TestCase, mock
 from django.views.generic import View
 
 
@@ -78,9 +78,9 @@ class AccessMixinTests(TestCase):
         with self.assertRaises(PermissionDenied):
             view(request)
 
+    @mock.patch.object(models.User, 'is_authenticated', False)
     def test_stacked_mixins_not_logged_in(self):
         user = models.User.objects.create(username='joe', password='qwerty')
-        user.is_authenticated = lambda: False
         perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser'))
         user.user_permissions.add(*perms)
         request = self.factory.get('/rand')

+ 7 - 7
tests/auth_tests/test_remote_user.py

@@ -38,15 +38,15 @@ class RemoteUserTest(TestCase):
         num_users = User.objects.count()
 
         response = self.client.get('/remote_user/')
-        self.assertTrue(response.context['user'].is_anonymous())
+        self.assertTrue(response.context['user'].is_anonymous)
         self.assertEqual(User.objects.count(), num_users)
 
         response = self.client.get('/remote_user/', **{self.header: None})
-        self.assertTrue(response.context['user'].is_anonymous())
+        self.assertTrue(response.context['user'].is_anonymous)
         self.assertEqual(User.objects.count(), num_users)
 
         response = self.client.get('/remote_user/', **{self.header: ''})
-        self.assertTrue(response.context['user'].is_anonymous())
+        self.assertTrue(response.context['user'].is_anonymous)
         self.assertEqual(User.objects.count(), num_users)
 
     def test_unknown_user(self):
@@ -118,7 +118,7 @@ class RemoteUserTest(TestCase):
         self.assertEqual(response.context['user'].username, 'knownuser')
         # During the session, the REMOTE_USER header disappears. Should trigger logout.
         response = self.client.get('/remote_user/')
-        self.assertEqual(response.context['user'].is_anonymous(), True)
+        self.assertTrue(response.context['user'].is_anonymous)
         # verify the remoteuser middleware will not remove a user
         # authenticated via another backend
         User.objects.create_user(username='modeluser', password='foo')
@@ -148,7 +148,7 @@ class RemoteUserTest(TestCase):
     def test_inactive_user(self):
         User.objects.create(username='knownuser', is_active=False)
         response = self.client.get('/remote_user/', **{self.header: 'knownuser'})
-        self.assertTrue(response.context['user'].is_anonymous())
+        self.assertTrue(response.context['user'].is_anonymous)
 
 
 class RemoteUserNoCreateBackend(RemoteUserBackend):
@@ -167,7 +167,7 @@ class RemoteUserNoCreateTest(RemoteUserTest):
     def test_unknown_user(self):
         num_users = User.objects.count()
         response = self.client.get('/remote_user/', **{self.header: 'newuser'})
-        self.assertTrue(response.context['user'].is_anonymous())
+        self.assertTrue(response.context['user'].is_anonymous)
         self.assertEqual(User.objects.count(), num_users)
 
 
@@ -268,5 +268,5 @@ class PersistentRemoteUserTest(RemoteUserTest):
         self.assertEqual(response.context['user'].username, 'knownuser')
         # Should stay logged in if the REMOTE_USER header disappears.
         response = self.client.get('/remote_user/')
-        self.assertEqual(response.context['user'].is_anonymous(), False)
+        self.assertFalse(response.context['user'].is_anonymous)
         self.assertEqual(response.context['user'].username, 'knownuser')

+ 1 - 1
tests/urlpatterns_reverse/views.py

@@ -45,7 +45,7 @@ class LazyRedirectView(RedirectView):
     url = reverse_lazy('named-lazy-url-redirected-to')
 
 
-@user_passes_test(lambda u: u.is_authenticated(), login_url=reverse_lazy('some-login-page'))
+@user_passes_test(lambda u: u.is_authenticated, login_url=reverse_lazy('some-login-page'))
 def login_required_view(request):
     return HttpResponse('Hello you')