Browse Source

Fixed #3011 -- Added swappable auth.User models.

Thanks to the many people that contributed to the development and review of
this patch, including (but not limited to) Jacob Kaplan-Moss, Anssi
Kääriäinen, Ramiro Morales, Preston Holmes, Josh Ourisman, Thomas Sutton,
and Roger Barnes, as well as the many, many people who have contributed to
the design discussion around this ticket over many years.

Squashed commit of the following:

commit d84749a0f034a0a6906d20df047086b1219040d0
Merge: 531e771 7c11b1a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Wed Sep 26 18:37:04 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit 531e7715da545f930c49919a19e954d41c59b446
Merge: 29d1abb 1f84b04
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Wed Sep 26 07:09:23 2012 +0800

    Merged recent trunk changes.

commit 29d1abbe351fd5da855fe5ce09e24227d90ddc91
Merge: 8a527dd 54c81a1
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Sep 24 07:49:46 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit 8a527dda13c9bec955b1f7e8db5822d1d9b32a01
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Sep 24 07:48:05 2012 +0800

    Ensure sequences are reset correctly in the presence of swapped models.

commit e2b6e22f298eb986d74d28b8d9906f37f5ff8eb8
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 23 17:53:05 2012 +0800

    Modifications to the handling and docs for auth forms.

commit 98aba856b534620aea9091f824b442b47d2fdb3c
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 23 15:28:57 2012 +0800

    Improved error handling and docs for get_user_model()

commit 0229209c844f06dfeb33b0b8eeec000c127695b6
Merge: 6494bf9 8599f64
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 23 14:50:11 2012 +0800

    Merged recent Django trunk changes.

commit 6494bf91f2ddaaabec3ec017f2e3131937c35517
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Sep 17 21:38:44 2012 +0800

    Improved validation of swappable model settings.

commit 5a04cde342cc860384eb844cfda5af55204564ad
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Sep 17 07:15:14 2012 +0800

    Removed some unused imports.

commit ffd535e4136dc54f084b6ac467e81444696e1c8a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 20:31:28 2012 +0800

    Corrected attribute access on for get_by_natural_key

commit 913e1ac84c3d9c7c58a9b3bdbbb15ebccd8a8c0a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 20:12:34 2012 +0800

    Added test for proxy model safeguards on swappable models.

commit 280bf19e94d0d534d0e51bae485c1842558f4ff4
Merge: dbb3900 935a863
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 18:16:49 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit dbb3900775a99df8b6cb1d7063cf364eab55621a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 18:09:27 2012 +0800

    Fixes for Python 3 compatibility.

commit dfd72131d8664615e245aa0f95b82604ba6b3821
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 15:54:30 2012 +0800

    Added protection against proxying swapped models.

commit abcb027190e53613e7f1734e77ee185b2587de31
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 15:11:10 2012 +0800

    Cleanup and documentation of AbstractUser base class.

commit a9491a87763e307f0eb0dc246f54ac865a6ffb34
Merge: fd8bb4e 08bcb4a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 14:46:49 2012 +0800

    Merge commit '08bcb4aec1ed154cefc631b8510ee13e9af0c19d' into t3011

commit fd8bb4e3e498a92d7a8b340f0684d5f088aa4c92
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 14:20:14 2012 +0800

    Documentation improvements coming from community review.

commit b550a6d06d016ab6a0198c4cb2dffe9cceabe8a5
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 13:52:47 2012 +0800

    Refactored skipIfCustomUser into the contrib.auth tests.

commit 52a02f11107c3f0d711742b8ca65b75175b79d6a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 13:46:10 2012 +0800

    Refactored common 'get' pattern into manager method.

commit b441a6bbc7d6065175715cb09316b9f13268171b
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 16 13:41:33 2012 +0800

    Added note about backwards incompatible change to admin login messages.

commit 08bcb4aec1ed154cefc631b8510ee13e9af0c19d
Author: Anssi Kääriäinen <akaariai@gmail.com>
Date:   Sat Sep 15 18:30:33 2012 +0300

    Splitted User to AbstractUser and User

commit d9f5e5addbad5e1a01f67e7358e4f5091c3cad81
Author: Anssi Kääriäinen <akaariai@gmail.com>
Date:   Sat Sep 15 18:30:02 2012 +0300

    Reworked REQUIRED_FIELDS + create_user() interaction

commit 579f152e4a6e06671e1ac1e59e2b43cf4d764bf4
Merge: 9184972 93e6733
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 15 20:18:37 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit 918497218c58227f5032873ff97261627b2ceab2
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 15 20:18:19 2012 +0800

    Deprecate AUTH_PROFILE_MODULE and get_profile().

commit 334cdfc1bb6a6794791497cdefda843bca2ea57a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 15 20:00:12 2012 +0800

    Added release notes for new swappable User feature.

commit 5d7bb22e8d913b51aba1c3360e7af8b01b6c0ab6
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 15 19:59:49 2012 +0800

    Ensure swapped models can't be queried.

commit 57ac6e3d32605a67581e875b37ec5b2284711a32
Merge: f2ec915 abfba3b
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 15 14:31:54 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit f2ec915b20f81c8afeaa3df25f80689712f720f8
Merge: 1952656 5e99a3d
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 9 08:29:51 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit 19526563b54fa300785c49cfb625c0c6158ced67
Merge: 2c5e833 c4aa26a
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 9 08:22:26 2012 +0800

    Merge recent changes from master.

commit 2c5e833a30bef4305d55eacc0703533152f5c427
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 9 07:53:46 2012 +0800

    Corrected admin_views tests following removal of the email fallback on admin logins.

commit 20d1892491839d6ef21f37db4ca136935c2076bf
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sun Sep 9 01:00:37 2012 +0800

    Added conditional skips for all tests dependent on the default User model

commit 40ea8b888284775481fc1eaadeff267dbd7e3dfa
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 8 23:47:02 2012 +0800

    Added documentation for REQUIRED_FIELDS in custom auth.

commit e6aaf659708cf6491f5485d3edfa616cb9214cc0
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Sat Sep 8 23:20:02 2012 +0800

    Added first draft of custom User docs.

    Thanks to Greg Turner for the initial text.

commit 75118bd242eec87649da2859e8c50a199a8a1dca
Author: Thomas Sutton <me@thomas-sutton.id.au>
Date:   Mon Aug 20 11:17:26 2012 +0800

    Admin app should not allow username discovery

    The admin app login form should not allow users to discover the username
    associated with an email address.

commit d088b3af58dad7449fc58493193a327725c57c22
Author: Thomas Sutton <me@thomas-sutton.id.au>
Date:   Mon Aug 20 10:32:13 2012 +0800

    Admin app login form should use swapped user model

commit 7e82e83d67ee0871a72e1a3a723afdd214fcefc3
Merge: e29c010 39aa890
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Fri Sep 7 23:45:03 2012 +0800

    Merged master changes.

commit e29c010beb96ca07697c4e3e0c0d5d3ffdc4c0a3
Merge: 8e3fd70 30bdf22
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Aug 20 13:12:57 2012 +0800

    Merge remote-tracking branch 'django/master' into t3011

commit 8e3fd703d02c31a4c3ac9f51f5011d03c0bd47f6
Merge: 507bb50 26e0ba0
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Aug 20 13:09:09 2012 +0800

    Merged recent changes from trunk.

commit 507bb50a9291bfcdcfa1198f9fea21d4e3b1e762
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Jun 4 20:41:37 2012 +0800

    Modified auth app so that login with alternate auth app is possible.

commit dabe3628362ab7a4a6c9686dd874803baa997eaa
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Jun 4 20:10:51 2012 +0800

    Modified auth management commands to handle custom user definitions.

commit 7cc0baf89d490c92ef3f1dc909b8090191a1294b
Author: Russell Keith-Magee <russell@keith-magee.com>
Date:   Mon Jun 4 14:17:28 2012 +0800

    Added model Meta option for swappable models, and made auth.User a swappable model
Russell Keith-Magee 12 năm trước cách đây
mục cha
commit
70a0de37d1
57 tập tin đã thay đổi với 1412 bổ sung343 xóa
  1. 2 0
      django/conf/global_settings.py
  2. 2 13
      django/contrib/admin/forms.py
  3. 4 2
      django/contrib/admin/models.py
  4. 20 14
      django/contrib/admin/sites.py
  5. 1 1
      django/contrib/admin/templates/admin/base.html
  6. 1 1
      django/contrib/admin/templates/admin/login.html
  7. 1 0
      django/contrib/admin/views/decorators.py
  8. 22 1
      django/contrib/auth/__init__.py
  9. 15 9
      django/contrib/auth/backends.py
  10. 14 0
      django/contrib/auth/fixtures/custom_user.json
  11. 11 4
      django/contrib/auth/forms.py
  12. 21 11
      django/contrib/auth/management/__init__.py
  13. 10 6
      django/contrib/auth/management/commands/changepassword.py
  14. 63 59
      django/contrib/auth/management/commands/createsuperuser.py
  15. 107 66
      django/contrib/auth/models.py
  16. 13 24
      django/contrib/auth/tests/__init__.py
  17. 5 0
      django/contrib/auth/tests/auth_backends.py
  18. 38 7
      django/contrib/auth/tests/basic.py
  19. 2 1
      django/contrib/auth/tests/context_processors.py
  20. 75 0
      django/contrib/auth/tests/custom_user.py
  21. 3 1
      django/contrib/auth/tests/decorators.py
  22. 7 1
      django/contrib/auth/tests/forms.py
  23. 99 1
      django/contrib/auth/tests/management.py
  24. 6 0
      django/contrib/auth/tests/models.py
  25. 4 0
      django/contrib/auth/tests/remote_user.py
  26. 2 0
      django/contrib/auth/tests/signals.py
  27. 2 0
      django/contrib/auth/tests/tokens.py
  28. 9 0
      django/contrib/auth/tests/utils.py
  29. 30 0
      django/contrib/auth/tests/views.py
  30. 1 0
      django/contrib/auth/tokens.py
  31. 14 5
      django/contrib/auth/views.py
  32. 11 6
      django/contrib/comments/models.py
  33. 12 1
      django/core/exceptions.py
  34. 1 0
      django/core/management/commands/sqlall.py
  35. 1 1
      django/core/management/commands/syncdb.py
  36. 1 0
      django/core/management/commands/validate.py
  37. 10 2
      django/core/management/sql.py
  38. 24 3
      django/core/management/validation.py
  39. 21 12
      django/core/validators.py
  40. 8 2
      django/db/backends/__init__.py
  41. 8 8
      django/db/backends/creation.py
  42. 14 6
      django/db/models/base.py
  43. 27 8
      django/db/models/fields/related.py
  44. 1 0
      django/db/models/loading.py
  45. 7 2
      django/db/models/manager.py
  46. 18 3
      django/db/models/options.py
  47. 2 1
      django/test/__init__.py
  48. 2 0
      django/test/testcases.py
  49. 5 2
      django/test/utils.py
  50. 3 0
      docs/internals/deprecation.txt
  51. 21 6
      docs/ref/settings.txt
  52. 42 0
      docs/releases/1.5.txt
  53. 359 0
      docs/topics/auth.txt
  54. 106 4
      tests/modeltests/invalid_models/invalid_models/models.py
  55. 11 2
      tests/modeltests/invalid_models/tests.py
  56. 36 2
      tests/modeltests/proxy_models/tests.py
  57. 57 45
      tests/regressiontests/admin_views/tests.py

+ 2 - 0
django/conf/global_settings.py

@@ -488,6 +488,8 @@ PROFANITIES_LIST = ()
 # AUTHENTICATION #
 ##################
 
+AUTH_USER_MODEL = 'auth.User'
+
 AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
 
 LOGIN_URL = '/accounts/login/'

+ 2 - 13
django/contrib/admin/forms.py

@@ -4,12 +4,12 @@ from django import forms
 
 from django.contrib.auth import authenticate
 from django.contrib.auth.forms import AuthenticationForm
-from django.contrib.auth.models import User
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext_lazy
 
 ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
         "for a staff account. Note that both fields are case-sensitive.")
 
+
 class AdminAuthenticationForm(AuthenticationForm):
     """
     A custom authentication form used in the admin app.
@@ -26,17 +26,6 @@ class AdminAuthenticationForm(AuthenticationForm):
         if username and password:
             self.user_cache = authenticate(username=username, password=password)
             if self.user_cache is None:
-                if '@' in username:
-                    # Mistakenly entered e-mail address instead of username? Look it up.
-                    try:
-                        user = User.objects.get(email=username)
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
-                        # Nothing to do here, moving along.
-                        pass
-                    else:
-                        if user.check_password(password):
-                            message = _("Your e-mail address is not your username."
-                                        " Try '%s' instead.") % user.username
                 raise forms.ValidationError(message)
             elif not self.user_cache.is_active or not self.user_cache.is_staff:
                 raise forms.ValidationError(message)

+ 4 - 2
django/contrib/admin/models.py

@@ -1,8 +1,8 @@
 from __future__ import unicode_literals
 
 from django.db import models
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.auth.models import User
 from django.contrib.admin.util import quote
 from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_text
@@ -12,15 +12,17 @@ ADDITION = 1
 CHANGE = 2
 DELETION = 3
 
+
 class LogEntryManager(models.Manager):
     def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
         e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message)
         e.save()
 
+
 @python_2_unicode_compatible
 class LogEntry(models.Model):
     action_time = models.DateTimeField(_('action time'), auto_now=True)
-    user = models.ForeignKey(User)
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
     content_type = models.ForeignKey(ContentType, blank=True, null=True)
     object_id = models.TextField(_('object id'), blank=True, null=True)
     object_repr = models.CharField(_('object repr'), max_length=200)

+ 20 - 14
django/contrib/admin/sites.py

@@ -9,7 +9,6 @@ from django.db.models.base import ModelBase
 from django.core.exceptions import ImproperlyConfigured
 from django.core.urlresolvers import reverse, NoReverseMatch
 from django.template.response import TemplateResponse
-from django.utils.safestring import mark_safe
 from django.utils import six
 from django.utils.text import capfirst
 from django.utils.translation import ugettext as _
@@ -18,12 +17,15 @@ from django.conf import settings
 
 LOGIN_FORM_KEY = 'this_is_the_login_form'
 
+
 class AlreadyRegistered(Exception):
     pass
 
+
 class NotRegistered(Exception):
     pass
 
+
 class AdminSite(object):
     """
     An AdminSite object encapsulates an instance of the Django admin application, ready
@@ -41,7 +43,7 @@ class AdminSite(object):
     password_change_done_template = None
 
     def __init__(self, name='admin', app_name='admin'):
-        self._registry = {} # model_class class -> admin_class instance
+        self._registry = {}  # model_class class -> admin_class instance
         self.name = name
         self.app_name = app_name
         self._actions = {'delete_selected': actions.delete_selected}
@@ -80,20 +82,23 @@ class AdminSite(object):
             if model in self._registry:
                 raise AlreadyRegistered('The model %s is already registered' % model.__name__)
 
-            # If we got **options then dynamically construct a subclass of
-            # admin_class with those **options.
-            if options:
-                # For reasons I don't quite understand, without a __module__
-                # the created class appears to "live" in the wrong place,
-                # which causes issues later on.
-                options['__module__'] = __name__
-                admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
+            # Ignore the registration if the model has been
+            # swapped out.
+            if not model._meta.swapped:
+                # If we got **options then dynamically construct a subclass of
+                # admin_class with those **options.
+                if options:
+                    # For reasons I don't quite understand, without a __module__
+                    # the created class appears to "live" in the wrong place,
+                    # which causes issues later on.
+                    options['__module__'] = __name__
+                    admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
 
-            # Validate (which might be a no-op)
-            validate(admin_class, model)
+                # Validate (which might be a no-op)
+                validate(admin_class, model)
 
-            # Instantiate the admin class to save in the registry
-            self._registry[model] = admin_class(model, self)
+                # Instantiate the admin class to save in the registry
+                self._registry[model] = admin_class(model, self)
 
     def unregister(self, model_or_iterable):
         """
@@ -319,6 +324,7 @@ class AdminSite(object):
             REDIRECT_FIELD_NAME: request.get_full_path(),
         }
         context.update(extra_context or {})
+
         defaults = {
             'extra_context': context,
             'current_app': self.name,

+ 1 - 1
django/contrib/admin/templates/admin/base.html

@@ -26,7 +26,7 @@
         {% if user.is_active and user.is_staff %}
         <div id="user-tools">
             {% trans 'Welcome,' %}
-            <strong>{% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}</strong>.
+            <strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong>.
             {% block userlinks %}
                 {% url 'django-admindocs-docroot' as docsroot %}
                 {% if docsroot %}

+ 1 - 1
django/contrib/admin/templates/admin/login.html

@@ -30,7 +30,7 @@
 <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
   <div class="form-row">
     {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
-    <label for="id_username" class="required">{% trans 'Username:' %}</label> {{ form.username }}
+    <label for="id_username" class="required">{{ form.username.label }}:</label> {{ form.username }}
   </div>
   <div class="form-row">
     {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}

+ 1 - 0
django/contrib/admin/views/decorators.py

@@ -4,6 +4,7 @@ from django.contrib.admin.forms import AdminAuthenticationForm
 from django.contrib.auth.views import login
 from django.contrib.auth import REDIRECT_FIELD_NAME
 
+
 def staff_member_required(view_func):
     """
     Decorator for views that checks that the user is logged in and is a staff

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

@@ -6,9 +6,10 @@ SESSION_KEY = '_auth_user_id'
 BACKEND_SESSION_KEY = '_auth_user_backend'
 REDIRECT_FIELD_NAME = 'next'
 
+
 def load_backend(path):
     i = path.rfind('.')
-    module, attr = path[:i], path[i+1:]
+    module, attr = path[:i], path[i + 1:]
     try:
         mod = import_module(module)
     except ImportError as e:
@@ -21,6 +22,7 @@ def load_backend(path):
         raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr))
     return cls()
 
+
 def get_backends():
     from django.conf import settings
     backends = []
@@ -30,6 +32,7 @@ def get_backends():
         raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?')
     return backends
 
+
 def authenticate(**credentials):
     """
     If the given credentials are valid, return a User object.
@@ -46,6 +49,7 @@ def authenticate(**credentials):
         user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
         return user
 
+
 def login(request, user):
     """
     Persist a user id and a backend in the request. This way a user doesn't
@@ -69,6 +73,7 @@ def login(request, user):
         request.user = user
     user_logged_in.send(sender=user.__class__, request=request, user=user)
 
+
 def logout(request):
     """
     Removes the authenticated user's ID from the request and flushes their
@@ -86,6 +91,22 @@ def logout(request):
         from django.contrib.auth.models import AnonymousUser
         request.user = AnonymousUser()
 
+
+def get_user_model():
+    "Return the User model that is active in this project"
+    from django.conf import settings
+    from django.db.models import get_model
+
+    try:
+        app_label, model_name = settings.AUTH_USER_MODEL.split('.')
+    except ValueError:
+        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
+    user_model = get_model(app_label, model_name)
+    if user_model is None:
+        raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL)
+    return user_model
+
+
 def get_user(request):
     from django.contrib.auth.models import AnonymousUser
     try:

+ 15 - 9
django/contrib/auth/backends.py

@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
-
-from django.contrib.auth.models import User, Permission
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
 
 
 class ModelBackend(object):
@@ -12,10 +12,11 @@ class ModelBackend(object):
     # configurable.
     def authenticate(self, username=None, password=None):
         try:
-            user = User.objects.get(username=username)
+            UserModel = get_user_model()
+            user = UserModel.objects.get_by_natural_key(username)
             if user.check_password(password):
                 return user
-        except User.DoesNotExist:
+        except UserModel.DoesNotExist:
             return None
 
     def get_group_permissions(self, user_obj, obj=None):
@@ -60,8 +61,9 @@ class ModelBackend(object):
 
     def get_user(self, user_id):
         try:
-            return User.objects.get(pk=user_id)
-        except User.DoesNotExist:
+            UserModel = get_user_model()
+            return UserModel.objects.get(pk=user_id)
+        except UserModel.DoesNotExist:
             return None
 
 
@@ -94,17 +96,21 @@ class RemoteUserBackend(ModelBackend):
         user = None
         username = self.clean_username(remote_user)
 
+        UserModel = get_user_model()
+
         # Note that this could be accomplished in one try-except clause, but
         # instead we use get_or_create when creating unknown users since it has
         # built-in safeguards for multiple threads.
         if self.create_unknown_user:
-            user, created = User.objects.get_or_create(username=username)
+            user, created = UserModel.objects.get_or_create(**{
+                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
+            })
             if created:
                 user = self.configure_user(user)
         else:
             try:
-                user = User.objects.get(username=username)
-            except User.DoesNotExist:
+                user = UserModel.objects.get_by_natural_key(username)
+            except UserModel.DoesNotExist:
                 pass
         return user
 

+ 14 - 0
django/contrib/auth/fixtures/custom_user.json

@@ -0,0 +1,14 @@
+[
+    {
+        "pk": "1",
+        "model": "auth.customuser",
+        "fields": {
+            "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+            "last_login": "2006-12-17 07:03:31",
+            "email": "staffmember@example.com",
+            "is_active": true,
+            "is_admin": false,
+            "date_of_birth": "1976-11-08"
+       }
+    }
+]

+ 11 - 4
django/contrib/auth/forms.py

@@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict
 from django.utils.html import format_html, format_html_join
 from django.utils.http import int_to_base36
 from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
 from django.utils.translation import ugettext, ugettext_lazy as _
 
-from django.contrib.auth import authenticate
+from django.contrib.auth import authenticate, get_user_model
 from django.contrib.auth.models import User
 from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
 from django.contrib.auth.tokens import default_token_generator
@@ -135,7 +136,7 @@ class AuthenticationForm(forms.Form):
     Base class for authenticating users. Extend this to get a form that accepts
     username/password logins.
     """
-    username = forms.CharField(label=_("Username"), max_length=30)
+    username = forms.CharField(max_length=30)
     password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
 
     error_messages = {
@@ -157,6 +158,11 @@ class AuthenticationForm(forms.Form):
         self.user_cache = None
         super(AuthenticationForm, self).__init__(*args, **kwargs)
 
+        # Set the label for the "username" field.
+        UserModel = get_user_model()
+        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
+        self.fields['username'].label = capfirst(username_field.verbose_name)
+
     def clean(self):
         username = self.cleaned_data.get('username')
         password = self.cleaned_data.get('password')
@@ -198,9 +204,10 @@ class PasswordResetForm(forms.Form):
         """
         Validates that an active user exists with the given email address.
         """
+        UserModel = get_user_model()
         email = self.cleaned_data["email"]
-        self.users_cache = User.objects.filter(email__iexact=email,
-                                               is_active=True)
+        self.users_cache = UserModel.objects.filter(email__iexact=email,
+                                                    is_active=True)
         if not len(self.users_cache):
             raise forms.ValidationError(self.error_messages['unknown'])
         if any((user.password == UNUSABLE_PASSWORD)

+ 21 - 11
django/contrib/auth/management/__init__.py

@@ -6,9 +6,10 @@ from __future__ import unicode_literals
 import getpass
 import locale
 import unicodedata
-from django.contrib.auth import models as auth_app
+
+from django.contrib.auth import models as auth_app, get_user_model
+from django.core import exceptions
 from django.db.models import get_models, signals
-from django.contrib.auth.models import User
 from django.utils import six
 from django.utils.six.moves import input
 
@@ -64,7 +65,9 @@ def create_permissions(app, created_models, verbosity, **kwargs):
 def create_superuser(app, created_models, verbosity, db, **kwargs):
     from django.core.management import call_command
 
-    if auth_app.User in created_models and kwargs.get('interactive', True):
+    UserModel = get_user_model()
+
+    if UserModel in created_models and kwargs.get('interactive', True):
         msg = ("\nYou just installed Django's auth system, which means you "
             "don't have any superusers defined.\nWould you like to create one "
             "now? (yes/no): ")
@@ -113,28 +116,35 @@ def get_default_username(check_db=True):
     :returns: The username, or an empty string if no username can be
         determined.
     """
-    from django.contrib.auth.management.commands.createsuperuser import (
-        RE_VALID_USERNAME)
+    # If the User model has been swapped out, we can't make any assumptions
+    # about the default user name.
+    if auth_app.User._meta.swapped:
+        return ''
+
     default_username = get_system_username()
     try:
         default_username = unicodedata.normalize('NFKD', default_username)\
             .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower()
     except UnicodeDecodeError:
         return ''
-    if not RE_VALID_USERNAME.match(default_username):
+
+    # Run the username validator
+    try:
+        auth_app.User._meta.get_field('username').run_validators(default_username)
+    except exceptions.ValidationError:
         return ''
+
     # Don't return the default username if it is already taken.
     if check_db and default_username:
         try:
-            User.objects.get(username=default_username)
-        except User.DoesNotExist:
+            auth_app.User.objects.get(username=default_username)
+        except auth_app.User.DoesNotExist:
             pass
         else:
             return ''
     return default_username
 
-
 signals.post_syncdb.connect(create_permissions,
-    dispatch_uid = "django.contrib.auth.management.create_permissions")
+    dispatch_uid="django.contrib.auth.management.create_permissions")
 signals.post_syncdb.connect(create_superuser,
-    sender=auth_app, dispatch_uid = "django.contrib.auth.management.create_superuser")
+    sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser")

+ 10 - 6
django/contrib/auth/management/commands/changepassword.py

@@ -1,8 +1,8 @@
 import getpass
 from optparse import make_option
 
+from django.contrib.auth import get_user_model
 from django.core.management.base import BaseCommand, CommandError
-from django.contrib.auth.models import User
 from django.db import DEFAULT_DB_ALIAS
 
 
@@ -30,12 +30,16 @@ class Command(BaseCommand):
         else:
             username = getpass.getuser()
 
+        UserModel = get_user_model()
+
         try:
-            u = User.objects.using(options.get('database')).get(username=username)
-        except User.DoesNotExist:
+            u = UserModel.objects.using(options.get('database')).get(**{
+                    getattr(UserModel, 'USERNAME_FIELD', 'username'): username
+                })
+        except UserModel.DoesNotExist:
             raise CommandError("user '%s' does not exist" % username)
 
-        self.stdout.write("Changing password for user '%s'\n" % u.username)
+        self.stdout.write("Changing password for user '%s'\n" % u)
 
         MAX_TRIES = 3
         count = 0
@@ -48,9 +52,9 @@ class Command(BaseCommand):
                 count = count + 1
 
         if count == MAX_TRIES:
-            raise CommandError("Aborting password change for user '%s' after %s attempts" % (username, count))
+            raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count))
 
         u.set_password(p1)
         u.save()
 
-        return "Password changed successfully for user '%s'" % u.username
+        return "Password changed successfully for user '%s'" % u

+ 63 - 59
django/contrib/auth/management/commands/createsuperuser.py

@@ -3,109 +3,114 @@ Management utility to create superusers.
 """
 
 import getpass
-import re
 import sys
 from optparse import make_option
 
-from django.contrib.auth.models import User
+from django.contrib.auth import get_user_model
 from django.contrib.auth.management import get_default_username
 from django.core import exceptions
 from django.core.management.base import BaseCommand, CommandError
 from django.db import DEFAULT_DB_ALIAS
 from django.utils.six.moves import input
-from django.utils.translation import ugettext as _
-
-RE_VALID_USERNAME = re.compile('[\w.@+-]+$')
-
-EMAIL_RE = re.compile(
-    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
-    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string
-    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
-
-
-def is_valid_email(value):
-    if not EMAIL_RE.search(value):
-        raise exceptions.ValidationError(_('Enter a valid e-mail address.'))
+from django.utils.text import capfirst
 
 
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
         make_option('--username', dest='username', default=None,
             help='Specifies the username for the superuser.'),
-        make_option('--email', dest='email', default=None,
-            help='Specifies the email address for the superuser.'),
         make_option('--noinput', action='store_false', dest='interactive', default=True,
             help=('Tells Django to NOT prompt the user for input of any kind. '
-                  'You must use --username and --email with --noinput, and '
-                  'superusers created with --noinput will not be able to log '
-                  'in until they\'re given a valid password.')),
+                  'You must use --username with --noinput, along with an option for '
+                  'any other required field. Superusers created with --noinput will '
+                  ' not be able to log in until they\'re given a valid password.')),
         make_option('--database', action='store', dest='database',
             default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'),
+    ) + tuple(
+        make_option('--%s' % field, dest=field, default=None,
+            help='Specifies the %s for the superuser.' % field)
+        for field in get_user_model().REQUIRED_FIELDS
     )
+
     help = 'Used to create a superuser.'
 
     def handle(self, *args, **options):
         username = options.get('username', None)
-        email = options.get('email', None)
         interactive = options.get('interactive')
         verbosity = int(options.get('verbosity', 1))
         database = options.get('database')
 
-        # Do quick and dirty validation if --noinput
-        if not interactive:
-            if not username or not email:
-                raise CommandError("You must use --username and --email with --noinput.")
-            if not RE_VALID_USERNAME.match(username):
-                raise CommandError("Invalid username. Use only letters, digits, and underscores")
-            try:
-                is_valid_email(email)
-            except exceptions.ValidationError:
-                raise CommandError("Invalid email address.")
+        UserModel = get_user_model()
+
+        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
+        other_fields = UserModel.REQUIRED_FIELDS
 
         # If not provided, create the user with an unusable password
         password = None
+        other_data = {}
 
-        # Prompt for username/email/password. Enclose this whole thing in a
-        # try/except to trap for a keyboard interrupt and exit gracefully.
-        if interactive:
+        # Do quick and dirty validation if --noinput
+        if not interactive:
+            try:
+                if not username:
+                    raise CommandError("You must use --username with --noinput.")
+                username = username_field.clean(username, None)
+
+                for field_name in other_fields:
+                    if options.get(field_name):
+                        field = UserModel._meta.get_field(field_name)
+                        other_data[field_name] = field.clean(options[field_name], None)
+                    else:
+                        raise CommandError("You must use --%s with --noinput." % field_name)
+            except exceptions.ValidationError as e:
+                raise CommandError('; '.join(e.messages))
+
+        else:
+            # Prompt for username/password, and any other required fields.
+            # Enclose this whole thing in a try/except to trap for a
+            # keyboard interrupt and exit gracefully.
             default_username = get_default_username()
             try:
 
                 # Get a username
-                while 1:
+                while username is None:
+                    username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
                     if not username:
-                        input_msg = 'Username'
+                        input_msg = capfirst(username_field.verbose_name)
                         if default_username:
                             input_msg += ' (leave blank to use %r)' % default_username
-                        username = input(input_msg + ': ')
-                    if default_username and username == '':
+                        raw_value = input(input_msg + ': ')
+                    if default_username and raw_value == '':
                         username = default_username
-                    if not RE_VALID_USERNAME.match(username):
-                        self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.")
+                    try:
+                        username = username_field.clean(raw_value, None)
+                    except exceptions.ValidationError as e:
+                        self.stderr.write("Error: %s" % '; '.join(e.messages))
                         username = None
                         continue
                     try:
-                        User.objects.using(database).get(username=username)
-                    except User.DoesNotExist:
-                        break
+                        UserModel.objects.using(database).get(**{
+                                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
+                            })
+                    except UserModel.DoesNotExist:
+                        pass
                     else:
                         self.stderr.write("Error: That username is already taken.")
                         username = None
 
-                # Get an email
-                while 1:
-                    if not email:
-                        email = input('E-mail address: ')
-                    try:
-                        is_valid_email(email)
-                    except exceptions.ValidationError:
-                        self.stderr.write("Error: That e-mail address is invalid.")
-                        email = None
-                    else:
-                        break
+                for field_name in other_fields:
+                    field = UserModel._meta.get_field(field_name)
+                    other_data[field_name] = options.get(field_name)
+                    while other_data[field_name] is None:
+                        raw_value = input(capfirst(field.verbose_name + ': '))
+                        try:
+                            other_data[field_name] = field.clean(raw_value, None)
+                        except exceptions.ValidationError as e:
+                            self.stderr.write("Error: %s" % '; '.join(e.messages))
+                            other_data[field_name] = None
 
                 # Get a password
-                while 1:
+                while password is None:
                     if not password:
                         password = getpass.getpass()
                         password2 = getpass.getpass('Password (again): ')
@@ -117,12 +122,11 @@ class Command(BaseCommand):
                         self.stderr.write("Error: Blank passwords aren't allowed.")
                         password = None
                         continue
-                    break
+
             except KeyboardInterrupt:
                 self.stderr.write("\nOperation cancelled.")
                 sys.exit(1)
 
-        User.objects.db_manager(database).create_superuser(username, email, password)
+        UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data)
         if verbosity >= 1:
-          self.stdout.write("Superuser created successfully.")
-
+            self.stdout.write("Superuser created successfully.")

+ 107 - 66
django/contrib/auth/models.py

@@ -1,7 +1,10 @@
 from __future__ import unicode_literals
+import re
+import warnings
 
 from django.core.exceptions import ImproperlyConfigured
 from django.core.mail import send_mail
+from django.core import validators
 from django.db import models
 from django.db.models.manager import EmptyManager
 from django.utils.crypto import get_random_string
@@ -96,6 +99,7 @@ class GroupManager(models.Manager):
     def get_by_natural_key(self, name):
         return self.get(name=name)
 
+
 @python_2_unicode_compatible
 class Group(models.Model):
     """
@@ -131,7 +135,7 @@ class Group(models.Model):
         return (self.name,)
 
 
-class UserManager(models.Manager):
+class BaseUserManager(models.Manager):
 
     @classmethod
     def normalize_email(cls, email):
@@ -148,7 +152,25 @@ class UserManager(models.Manager):
             email = '@'.join([email_name, domain_part.lower()])
         return email
 
-    def create_user(self, username, email=None, password=None):
+    def make_random_password(self, length=10,
+                             allowed_chars='abcdefghjkmnpqrstuvwxyz'
+                                           'ABCDEFGHJKLMNPQRSTUVWXYZ'
+                                           '23456789'):
+        """
+        Generates a random password with the given length and given
+        allowed_chars. Note that the default value of allowed_chars does not
+        have "I" or "O" or letters and digits that look similar -- just to
+        avoid confusion.
+        """
+        return get_random_string(length, allowed_chars)
+
+    def get_by_natural_key(self, username):
+        return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username})
+
+
+class UserManager(BaseUserManager):
+
+    def create_user(self, username, email=None, password=None, **extra_fields):
         """
         Creates and saves a User with the given username, email and password.
         """
@@ -158,35 +180,20 @@ class UserManager(models.Manager):
         email = UserManager.normalize_email(email)
         user = self.model(username=username, email=email,
                           is_staff=False, is_active=True, is_superuser=False,
-                          last_login=now, date_joined=now)
+                          last_login=now, date_joined=now, **extra_fields)
 
         user.set_password(password)
         user.save(using=self._db)
         return user
 
-    def create_superuser(self, username, email, password):
-        u = self.create_user(username, email, password)
+    def create_superuser(self, username, email, password, **extra_fields):
+        u = self.create_user(username, email, password, **extra_fields)
         u.is_staff = True
         u.is_active = True
         u.is_superuser = True
         u.save(using=self._db)
         return u
 
-    def make_random_password(self, length=10,
-                             allowed_chars='abcdefghjkmnpqrstuvwxyz'
-                                           'ABCDEFGHJKLMNPQRSTUVWXYZ'
-                                           '23456789'):
-        """
-        Generates a random password with the given length and given
-        allowed_chars. Note that the default value of allowed_chars does not
-        have "I" or "O" or letters and digits that look similar -- just to
-        avoid confusion.
-        """
-        return get_random_string(length, allowed_chars)
-
-    def get_by_natural_key(self, username):
-        return self.get(username=username)
-
 
 # A few helper functions for common logic between User and AnonymousUser.
 def _user_get_all_permissions(user, obj):
@@ -201,8 +208,6 @@ def _user_get_all_permissions(user, obj):
 
 
 def _user_has_perm(user, perm, obj):
-    anon = user.is_anonymous()
-    active = user.is_active
     for backend in auth.get_backends():
         if hasattr(backend, "has_perm"):
             if obj is not None:
@@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj):
 
 
 def _user_has_module_perms(user, app_label):
-    anon = user.is_anonymous()
-    active = user.is_active
     for backend in auth.get_backends():
         if hasattr(backend, "has_module_perms"):
             if backend.has_module_perms(user, app_label):
@@ -224,21 +227,73 @@ def _user_has_module_perms(user, app_label):
     return False
 
 
+class AbstractBaseUser(models.Model):
+    password = models.CharField(_('password'), max_length=128)
+    last_login = models.DateTimeField(_('last login'), default=timezone.now)
+
+    REQUIRED_FIELDS = []
+
+    class Meta:
+        abstract = True
+
+    def is_anonymous(self):
+        """
+        Always returns False. This is a way of comparing User objects to
+        anonymous users.
+        """
+        return False
+
+    def is_authenticated(self):
+        """
+        Always return True. This is a way to tell if the user has been
+        authenticated in templates.
+        """
+        return True
+
+    def set_password(self, raw_password):
+        self.password = make_password(raw_password)
+
+    def check_password(self, raw_password):
+        """
+        Returns a boolean of whether the raw_password was correct. Handles
+        hashing formats behind the scenes.
+        """
+        def setter(raw_password):
+            self.set_password(raw_password)
+            self.save(update_fields=["password"])
+        return check_password(raw_password, self.password, setter)
+
+    def set_unusable_password(self):
+        # Sets a value that will never be a valid hash
+        self.password = make_password(None)
+
+    def has_usable_password(self):
+        return is_password_usable(self.password)
+
+    def get_full_name(self):
+        raise NotImplementedError()
+
+    def get_short_name(self):
+        raise NotImplementedError()
+
+
 @python_2_unicode_compatible
-class User(models.Model):
+class AbstractUser(AbstractBaseUser):
     """
-    Users within the Django authentication system are represented by this
-    model.
+    An abstract base class implementing a fully featured User model with
+    admin-compliant permissions.
 
-    Username and password are required. Other fields are optional.
+    Username, password and email are required. Other fields are optional.
     """
     username = models.CharField(_('username'), max_length=30, unique=True,
         help_text=_('Required. 30 characters or fewer. Letters, numbers and '
-                    '@/./+/-/_ characters'))
+                    '@/./+/-/_ characters'),
+        validators=[
+            validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
+        ])
     first_name = models.CharField(_('first name'), max_length=30, blank=True)
     last_name = models.CharField(_('last name'), max_length=30, blank=True)
-    email = models.EmailField(_('e-mail address'), blank=True)
-    password = models.CharField(_('password'), max_length=128)
+    email = models.EmailField(_('email address'), blank=True)
     is_staff = models.BooleanField(_('staff status'), default=False,
         help_text=_('Designates whether the user can log into this admin '
                     'site.'))
@@ -248,7 +303,6 @@ class User(models.Model):
     is_superuser = models.BooleanField(_('superuser status'), default=False,
         help_text=_('Designates that this user has all permissions without '
                     'explicitly assigning them.'))
-    last_login = models.DateTimeField(_('last login'), default=timezone.now)
     date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
     groups = models.ManyToManyField(Group, verbose_name=_('groups'),
         blank=True, help_text=_('The groups this user belongs to. A user will '
@@ -257,11 +311,15 @@ class User(models.Model):
     user_permissions = models.ManyToManyField(Permission,
         verbose_name=_('user permissions'), blank=True,
         help_text='Specific permissions for this user.')
+
     objects = UserManager()
 
+    REQUIRED_FIELDS = ['email']
+
     class Meta:
         verbose_name = _('user')
         verbose_name_plural = _('users')
+        abstract = True
 
     def __str__(self):
         return self.username
@@ -272,20 +330,6 @@ class User(models.Model):
     def get_absolute_url(self):
         return "/users/%s/" % urlquote(self.username)
 
-    def is_anonymous(self):
-        """
-        Always returns False. This is a way of comparing User objects to
-        anonymous users.
-        """
-        return False
-
-    def is_authenticated(self):
-        """
-        Always return True. This is a way to tell if the user has been
-        authenticated in templates.
-        """
-        return True
-
     def get_full_name(self):
         """
         Returns the first_name plus the last_name, with a space in between.
@@ -293,25 +337,9 @@ class User(models.Model):
         full_name = '%s %s' % (self.first_name, self.last_name)
         return full_name.strip()
 
-    def set_password(self, raw_password):
-        self.password = make_password(raw_password)
-
-    def check_password(self, raw_password):
-        """
-        Returns a boolean of whether the raw_password was correct. Handles
-        hashing formats behind the scenes.
-        """
-        def setter(raw_password):
-            self.set_password(raw_password)
-            self.save(update_fields=["password"])
-        return check_password(raw_password, self.password, setter)
-
-    def set_unusable_password(self):
-        # Sets a value that will never be a valid hash
-        self.password = make_password(None)
-
-    def has_usable_password(self):
-        return is_password_usable(self.password)
+    def get_short_name(self):
+        "Returns the short name for the user."
+        return self.first_name
 
     def get_group_permissions(self, obj=None):
         """
@@ -381,6 +409,8 @@ class User(models.Model):
         Returns site-specific profile for this user. Raises
         SiteProfileNotAvailable if this site does not allow profiles.
         """
+        warnings.warn("The use of AUTH_PROFILE_MODULE to define user profiles has been deprecated.",
+            PendingDeprecationWarning)
         if not hasattr(self, '_profile_cache'):
             from django.conf import settings
             if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
@@ -407,6 +437,17 @@ class User(models.Model):
         return self._profile_cache
 
 
+class User(AbstractUser):
+    """
+    Users within the Django authentication system are represented by this
+    model.
+
+    Username, password and email are required. Other fields are optional.
+    """
+    class Meta:
+        swappable = 'AUTH_USER_MODEL'
+
+
 @python_2_unicode_compatible
 class AnonymousUser(object):
     id = None
@@ -431,7 +472,7 @@ class AnonymousUser(object):
         return not self.__eq__(other)
 
     def __hash__(self):
-        return 1 # instances always return the same hash value
+        return 1  # instances always return the same hash value
 
     def save(self):
         raise NotImplementedError

+ 13 - 24
django/contrib/auth/tests/__init__.py

@@ -1,26 +1,15 @@
-from django.contrib.auth.tests.auth_backends import (BackendTest,
-    RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
-    InActiveUserBackendTest)
-from django.contrib.auth.tests.basic import BasicTestCase
-from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
-from django.contrib.auth.tests.decorators import LoginRequiredTestCase
-from django.contrib.auth.tests.forms import (UserCreationFormTest,
-    AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest,
-    UserChangeFormTest, PasswordResetFormTest)
-from django.contrib.auth.tests.remote_user import (RemoteUserTest,
-    RemoteUserNoCreateTest, RemoteUserCustomTest)
-from django.contrib.auth.tests.management import (
-    GetDefaultUsernameTestCase,
-    ChangepasswordManagementCommandTestCase,
-)
-from django.contrib.auth.tests.models import (ProfileTestCase, NaturalKeysTestCase,
-    LoadDataWithoutNaturalKeysTestCase, LoadDataWithNaturalKeysTestCase,
-    UserManagerTestCase)
-from django.contrib.auth.tests.hashers import TestUtilsHashPass
-from django.contrib.auth.tests.signals import SignalTestCase
-from django.contrib.auth.tests.tokens import TokenGeneratorTest
-from django.contrib.auth.tests.views import (AuthViewNamedURLTests,
-    PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest,
-    LoginURLSettings)
+from django.contrib.auth.tests.custom_user import *
+from django.contrib.auth.tests.auth_backends import *
+from django.contrib.auth.tests.basic import *
+from django.contrib.auth.tests.context_processors import *
+from django.contrib.auth.tests.decorators import *
+from django.contrib.auth.tests.forms import *
+from django.contrib.auth.tests.remote_user import *
+from django.contrib.auth.tests.management import *
+from django.contrib.auth.tests.models import *
+from django.contrib.auth.tests.hashers import *
+from django.contrib.auth.tests.signals import *
+from django.contrib.auth.tests.tokens import *
+from django.contrib.auth.tests.views import *
 
 # The password for the fixture data users is 'password'

+ 5 - 0
django/contrib/auth/tests/auth_backends.py

@@ -2,12 +2,14 @@ from __future__ import unicode_literals
 
 from django.conf import settings
 from django.contrib.auth.models import User, Group, Permission, AnonymousUser
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ImproperlyConfigured
 from django.test import TestCase
 from django.test.utils import override_settings
 
 
+@skipIfCustomUser
 class BackendTest(TestCase):
 
     backend = 'django.contrib.auth.backends.ModelBackend'
@@ -151,6 +153,7 @@ class SimpleRowlevelBackend(object):
             return ['none']
 
 
+@skipIfCustomUser
 class RowlevelBackendTest(TestCase):
     """
     Tests for auth backend that supports object level permissions
@@ -223,6 +226,7 @@ class AnonymousUserBackendTest(TestCase):
         self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon']))
 
 
+@skipIfCustomUser
 @override_settings(AUTHENTICATION_BACKENDS=[])
 class NoBackendsTest(TestCase):
     """
@@ -235,6 +239,7 @@ class NoBackendsTest(TestCase):
         self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),))
 
 
+@skipIfCustomUser
 class InActiveUserBackendTest(TestCase):
     """
     Tests for a inactive user

+ 38 - 7
django/contrib/auth/tests/basic.py

@@ -1,13 +1,18 @@
 import locale
-import traceback
 
+from django.contrib.auth import get_user_model
 from django.contrib.auth.management.commands import createsuperuser
 from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth.tests.custom_user import CustomUser
+from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.core.exceptions import ImproperlyConfigured
 from django.core.management import call_command
 from django.test import TestCase
+from django.test.utils import override_settings
 from django.utils.six import StringIO
 
 
+@skipIfCustomUser
 class BasicTestCase(TestCase):
     def test_user(self):
         "Check that users can be created and can set their password"
@@ -33,7 +38,7 @@ class BasicTestCase(TestCase):
         self.assertFalse(u.is_superuser)
 
         # Check API-based user creation with no password
-        u2 = User.objects.create_user('testuser2', 'test2@example.com')
+        User.objects.create_user('testuser2', 'test2@example.com')
         self.assertFalse(u.has_usable_password())
 
     def test_user_no_email(self):
@@ -98,7 +103,6 @@ class BasicTestCase(TestCase):
         self.assertEqual(u.email, 'joe2@somewhere.org')
         self.assertFalse(u.has_usable_password())
 
-
         new_io = StringIO()
         call_command("createsuperuser",
             interactive=False,
@@ -124,15 +128,21 @@ class BasicTestCase(TestCase):
 
             # Temporarily replace getpass to allow interactive code to be used
             # non-interactively
-            class mock_getpass: pass
+            class mock_getpass:
+                pass
             mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd")
             createsuperuser.getpass = mock_getpass
 
             # Call the command in this new environment
             new_io = StringIO()
-            call_command("createsuperuser", interactive=True, username="nolocale@somewhere.org", email="nolocale@somewhere.org", stdout=new_io)
-
-        except TypeError as e:
+            call_command("createsuperuser",
+                interactive=True,
+                username="nolocale@somewhere.org",
+                email="nolocale@somewhere.org",
+                stdout=new_io
+            )
+
+        except TypeError:
             self.fail("createsuperuser fails if the OS provides no information about the current locale")
 
         finally:
@@ -143,3 +153,24 @@ class BasicTestCase(TestCase):
         # If we were successful, a user should have been created
         u = User.objects.get(username="nolocale@somewhere.org")
         self.assertEqual(u.email, 'nolocale@somewhere.org')
+
+    def test_get_user_model(self):
+        "The current user model can be retrieved"
+        self.assertEqual(get_user_model(), User)
+
+    @override_settings(AUTH_USER_MODEL='auth.CustomUser')
+    def test_swappable_user(self):
+        "The current user model can be swapped out for another"
+        self.assertEqual(get_user_model(), CustomUser)
+
+    @override_settings(AUTH_USER_MODEL='badsetting')
+    def test_swappable_user_bad_setting(self):
+        "The alternate user setting must point to something in the format app.model"
+        with self.assertRaises(ImproperlyConfigured):
+            get_user_model()
+
+    @override_settings(AUTH_USER_MODEL='thismodel.doesntexist')
+    def test_swappable_user_nonexistent_model(self):
+        "The current user model must point to an installed model"
+        with self.assertRaises(ImproperlyConfigured):
+            get_user_model()

+ 2 - 1
django/contrib/auth/tests/context_processors.py

@@ -2,12 +2,13 @@ import os
 
 from django.conf import global_settings
 from django.contrib.auth import authenticate
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.db.models import Q
-from django.template import context
 from django.test import TestCase
 from django.test.utils import override_settings
 
 
+@skipIfCustomUser
 @override_settings(
     TEMPLATE_DIRS=(
             os.path.join(os.path.dirname(__file__), 'templates'),

+ 75 - 0
django/contrib/auth/tests/custom_user.py

@@ -0,0 +1,75 @@
+# The custom User uses email as the unique identifier, and requires
+# that every user provide a date of birth. This lets us test
+# changes in username datatype, and non-text required fields.
+
+from django.db import models
+from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
+
+
+class CustomUserManager(BaseUserManager):
+    def create_user(self, email, date_of_birth, password=None):
+        """
+        Creates and saves a User with the given email and password.
+        """
+        if not email:
+            raise ValueError('Users must have an email address')
+
+        user = self.model(
+            email=CustomUserManager.normalize_email(email),
+            date_of_birth=date_of_birth,
+        )
+
+        user.set_password(password)
+        user.save(using=self._db)
+        return user
+
+    def create_superuser(self, username, password, date_of_birth):
+        u = self.create_user(username, password=password, date_of_birth=date_of_birth)
+        u.is_admin = True
+        u.save(using=self._db)
+        return u
+
+
+class CustomUser(AbstractBaseUser):
+    email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
+    is_active = models.BooleanField(default=True)
+    is_admin = models.BooleanField(default=False)
+    date_of_birth = models.DateField()
+
+    objects = CustomUserManager()
+
+    USERNAME_FIELD = 'email'
+    REQUIRED_FIELDS = ['date_of_birth']
+
+    class Meta:
+        app_label = 'auth'
+
+    def get_full_name(self):
+        return self.email
+
+    def get_short_name(self):
+        return self.email
+
+    def __unicode__(self):
+        return self.email
+
+    # Maybe required?
+    def get_group_permissions(self, obj=None):
+        return set()
+
+    def get_all_permissions(self, obj=None):
+        return set()
+
+    def has_perm(self, perm, obj=None):
+        return True
+
+    def has_perms(self, perm_list, obj=None):
+        return True
+
+    def has_module_perms(self, app_label):
+        return True
+
+    # Admin required fields
+    @property
+    def is_staff(self):
+        return self.is_admin

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

@@ -1,7 +1,9 @@
-from django.conf import settings
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.tests.views import AuthViewsTestCase
+from django.contrib.auth.tests.utils import skipIfCustomUser
 
+
+@skipIfCustomUser
 class LoginRequiredTestCase(AuthViewsTestCase):
     """
     Tests the login_required decorators

+ 7 - 1
django/contrib/auth/tests/forms.py

@@ -4,16 +4,17 @@ import os
 from django.contrib.auth.models import User
 from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
     PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm)
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.core import mail
 from django.forms.fields import Field, EmailField
 from django.test import TestCase
 from django.test.utils import override_settings
 from django.utils.encoding import force_text
-from django.utils import six
 from django.utils import translation
 from django.utils.translation import ugettext as _
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class UserCreationFormTest(TestCase):
 
@@ -81,6 +82,7 @@ class UserCreationFormTest(TestCase):
         self.assertEqual(repr(u), '<User: jsmith@example.com>')
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AuthenticationFormTest(TestCase):
 
@@ -133,6 +135,7 @@ class AuthenticationFormTest(TestCase):
         self.assertEqual(form.non_field_errors(), [])
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class SetPasswordFormTest(TestCase):
 
@@ -160,6 +163,7 @@ class SetPasswordFormTest(TestCase):
         self.assertTrue(form.is_valid())
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class PasswordChangeFormTest(TestCase):
 
@@ -208,6 +212,7 @@ class PasswordChangeFormTest(TestCase):
                          ['old_password', 'new_password1', 'new_password2'])
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class UserChangeFormTest(TestCase):
 
@@ -261,6 +266,7 @@ class UserChangeFormTest(TestCase):
             form.as_table())
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class PasswordResetFormTest(TestCase):
 

+ 99 - 1
django/contrib/auth/tests/management.py

@@ -1,13 +1,20 @@
 from __future__ import unicode_literals
+from datetime import date
 
 from django.contrib.auth import models, management
 from django.contrib.auth.management.commands import changepassword
+from django.contrib.auth.models import User
+from django.contrib.auth.tests import CustomUser
+from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.core.management import call_command
 from django.core.management.base import CommandError
 from django.test import TestCase
+from django.test.utils import override_settings
 from django.utils import six
 from django.utils.six import StringIO
 
 
+@skipIfCustomUser
 class GetDefaultUsernameTestCase(TestCase):
 
     def setUp(self):
@@ -36,6 +43,7 @@ class GetDefaultUsernameTestCase(TestCase):
         self.assertEqual(management.get_default_username(), 'julia')
 
 
+@skipIfCustomUser
 class ChangepasswordManagementCommandTestCase(TestCase):
 
     def setUp(self):
@@ -48,7 +56,7 @@ class ChangepasswordManagementCommandTestCase(TestCase):
         self.stderr.close()
 
     def test_that_changepassword_command_changes_joes_password(self):
-        " Executing the changepassword management command should change joe's password "
+        "Executing the changepassword management command should change joe's password"
         self.assertTrue(self.user.check_password('qwerty'))
         command = changepassword.Command()
         command._get_pass = lambda *args: 'not qwerty'
@@ -69,3 +77,93 @@ class ChangepasswordManagementCommandTestCase(TestCase):
 
         with self.assertRaises(CommandError):
             command.execute("joe", stdout=self.stdout, stderr=self.stderr)
+
+
+@skipIfCustomUser
+class CreatesuperuserManagementCommandTestCase(TestCase):
+
+    def test_createsuperuser(self):
+        "Check the operation of the createsuperuser management command"
+        # We can use the management command to create a superuser
+        new_io = StringIO()
+        call_command("createsuperuser",
+            interactive=False,
+            username="joe",
+            email="joe@somewhere.org",
+            stdout=new_io
+        )
+        command_output = new_io.getvalue().strip()
+        self.assertEqual(command_output, 'Superuser created successfully.')
+        u = User.objects.get(username="joe")
+        self.assertEqual(u.email, 'joe@somewhere.org')
+
+        # created password should be unusable
+        self.assertFalse(u.has_usable_password())
+
+    def test_verbosity_zero(self):
+        # We can supress output on the management command
+        new_io = StringIO()
+        call_command("createsuperuser",
+            interactive=False,
+            username="joe2",
+            email="joe2@somewhere.org",
+            verbosity=0,
+            stdout=new_io
+        )
+        command_output = new_io.getvalue().strip()
+        self.assertEqual(command_output, '')
+        u = User.objects.get(username="joe2")
+        self.assertEqual(u.email, 'joe2@somewhere.org')
+        self.assertFalse(u.has_usable_password())
+
+    def test_email_in_username(self):
+        new_io = StringIO()
+        call_command("createsuperuser",
+            interactive=False,
+            username="joe+admin@somewhere.org",
+            email="joe@somewhere.org",
+            stdout=new_io
+        )
+        u = User.objects.get(username="joe+admin@somewhere.org")
+        self.assertEqual(u.email, 'joe@somewhere.org')
+        self.assertFalse(u.has_usable_password())
+
+    @override_settings(AUTH_USER_MODEL='auth.CustomUser')
+    def test_swappable_user(self):
+        "A superuser can be created when a custom User model is in use"
+        # We can use the management command to create a superuser
+        # We skip validation because the temporary substitution of the
+        # swappable User model messes with validation.
+        new_io = StringIO()
+        call_command("createsuperuser",
+            interactive=False,
+            username="joe@somewhere.org",
+            date_of_birth="1976-04-01",
+            stdout=new_io,
+            skip_validation=True
+        )
+        command_output = new_io.getvalue().strip()
+        self.assertEqual(command_output, 'Superuser created successfully.')
+        u = CustomUser.objects.get(email="joe@somewhere.org")
+        self.assertEqual(u.date_of_birth, date(1976, 4, 1))
+
+        # created password should be unusable
+        self.assertFalse(u.has_usable_password())
+
+    @override_settings(AUTH_USER_MODEL='auth.CustomUser')
+    def test_swappable_user_missing_required_field(self):
+        "A superuser can be created when a custom User model is in use"
+        # We can use the management command to create a superuser
+        # We skip validation because the temporary substitution of the
+        # swappable User model messes with validation.
+        new_io = StringIO()
+        with self.assertRaises(CommandError):
+            call_command("createsuperuser",
+                interactive=False,
+                username="joe@somewhere.org",
+                stdout=new_io,
+                stderr=new_io,
+                skip_validation=True
+            )
+
+        self.assertEqual(CustomUser.objects.count(), 0)

+ 6 - 0
django/contrib/auth/tests/models.py

@@ -1,11 +1,13 @@
 from django.conf import settings
 from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable,
     UserManager)
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.test import TestCase
 from django.test.utils import override_settings
 from django.utils import six
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='')
 class ProfileTestCase(TestCase):
 
@@ -31,6 +33,7 @@ class ProfileTestCase(TestCase):
             user.get_profile()
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False)
 class NaturalKeysTestCase(TestCase):
     fixtures = ['authtestdata.json']
@@ -45,6 +48,7 @@ class NaturalKeysTestCase(TestCase):
         self.assertEqual(Group.objects.get_by_natural_key('users'), users_group)
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False)
 class LoadDataWithoutNaturalKeysTestCase(TestCase):
     fixtures = ['regular.json']
@@ -55,6 +59,7 @@ class LoadDataWithoutNaturalKeysTestCase(TestCase):
         self.assertEqual(group, user.groups.get())
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False)
 class LoadDataWithNaturalKeysTestCase(TestCase):
     fixtures = ['natural.json']
@@ -65,6 +70,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase):
         self.assertEqual(group, user.groups.get())
 
 
+@skipIfCustomUser
 class UserManagerTestCase(TestCase):
 
     def test_create_user(self):

+ 4 - 0
django/contrib/auth/tests/remote_user.py

@@ -3,10 +3,12 @@ from datetime import datetime
 from django.conf import settings
 from django.contrib.auth.backends import RemoteUserBackend
 from django.contrib.auth.models import User
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.test import TestCase
 from django.utils import timezone
 
 
+@skipIfCustomUser
 class RemoteUserTest(TestCase):
 
     urls = 'django.contrib.auth.tests.urls'
@@ -106,6 +108,7 @@ class RemoteUserNoCreateBackend(RemoteUserBackend):
     create_unknown_user = False
 
 
+@skipIfCustomUser
 class RemoteUserNoCreateTest(RemoteUserTest):
     """
     Contains the same tests as RemoteUserTest, but using a custom auth backend
@@ -142,6 +145,7 @@ class CustomRemoteUserBackend(RemoteUserBackend):
         return user
 
 
+@skipIfCustomUser
 class RemoteUserCustomTest(RemoteUserTest):
     """
     Tests a custom RemoteUserBackend subclass that overrides the clean_username

+ 2 - 0
django/contrib/auth/tests/signals.py

@@ -1,10 +1,12 @@
 from django.contrib.auth import signals
 from django.contrib.auth.models import User
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.test import TestCase
 from django.test.client import RequestFactory
 from django.test.utils import override_settings
 
 
+@skipIfCustomUser
 @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class SignalTestCase(TestCase):
     urls = 'django.contrib.auth.tests.urls'

+ 2 - 0
django/contrib/auth/tests/tokens.py

@@ -4,10 +4,12 @@ from datetime import date, timedelta
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.contrib.auth.tests.utils import skipIfCustomUser
 from django.test import TestCase
 from django.utils import unittest
 
 
+@skipIfCustomUser
 class TokenGeneratorTest(TestCase):
 
     def test_make_token(self):

+ 9 - 0
django/contrib/auth/tests/utils.py

@@ -0,0 +1,9 @@
+from django.conf import settings
+from django.utils.unittest import skipIf
+
+
+def skipIfCustomUser(test_func):
+    """
+    Skip a test if a custom user model is in use.
+    """
+    return skipIf(settings.AUTH_USER_MODEL != 'auth.User', 'Custom user model in use')(test_func)

+ 30 - 0
django/contrib/auth/tests/views.py

@@ -16,6 +16,7 @@ from django.test.utils import override_settings
 from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
 from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
                 SetPasswordForm, PasswordResetForm)
+from django.contrib.auth.tests.utils import skipIfCustomUser
 
 
 @override_settings(
@@ -50,6 +51,7 @@ class AuthViewsTestCase(TestCase):
         return self.assertContains(response, escape(force_text(text)), **kwargs)
 
 
+@skipIfCustomUser
 class AuthViewNamedURLTests(AuthViewsTestCase):
     urls = 'django.contrib.auth.urls'
 
@@ -75,6 +77,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase):
                 self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name)
 
 
+@skipIfCustomUser
 class PasswordResetTest(AuthViewsTestCase):
 
     def test_email_not_found(self):
@@ -172,6 +175,30 @@ class PasswordResetTest(AuthViewsTestCase):
         self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch'])
 
 
+@override_settings(AUTH_USER_MODEL='auth.CustomUser')
+class CustomUserPasswordResetTest(AuthViewsTestCase):
+    fixtures = ['custom_user.json']
+
+    def _test_confirm_start(self):
+        # Start by creating the email
+        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(len(mail.outbox), 1)
+        return self._read_signup_email(mail.outbox[0])
+
+    def _read_signup_email(self, email):
+        urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
+        self.assertTrue(urlmatch is not None, "No URL found in sent email")
+        return urlmatch.group(), urlmatch.groups()[0]
+
+    def test_confirm_valid_custom_user(self):
+        url, path = self._test_confirm_start()
+        response = self.client.get(path)
+        # redirect to a 'complete' page:
+        self.assertContains(response, "Please enter your new password")
+
+
+@skipIfCustomUser
 class ChangePasswordTest(AuthViewsTestCase):
 
     def fail_login(self, password='password'):
@@ -231,6 +258,7 @@ class ChangePasswordTest(AuthViewsTestCase):
             self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/'))
 
 
+@skipIfCustomUser
 class LoginTest(AuthViewsTestCase):
 
     def test_current_site_in_context_after_login(self):
@@ -289,6 +317,7 @@ class LoginTest(AuthViewsTestCase):
                             "%s should be allowed" % good_url)
 
 
+@skipIfCustomUser
 class LoginURLSettings(AuthViewsTestCase):
 
     def setUp(self):
@@ -347,6 +376,7 @@ class LoginURLSettings(AuthViewsTestCase):
                                                     querystring.urlencode('/')))
 
 
+@skipIfCustomUser
 class LogoutTest(AuthViewsTestCase):
 
     def confirm_logged_out(self):

+ 1 - 0
django/contrib/auth/tokens.py

@@ -4,6 +4,7 @@ from django.utils.http import int_to_base36, base36_to_int
 from django.utils.crypto import constant_time_compare, salted_hmac
 from django.utils import six
 
+
 class PasswordResetTokenGenerator(object):
     """
     Strategy object used to generate and check tokens for the password

+ 14 - 5
django/contrib/auth/views.py

@@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 
 # Avoid shadowing the login() and logout() views below.
-from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout
+from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm
-from django.contrib.auth.models import User
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.sites.models import get_current_site
 
@@ -74,6 +73,7 @@ def login(request, template_name='registration/login.html',
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 def logout(request, next_page=None,
            template_name='registration/logged_out.html',
            redirect_field_name=REDIRECT_FIELD_NAME,
@@ -104,6 +104,7 @@ def logout(request, next_page=None,
         # Redirect to this page until the session has been cleared.
         return HttpResponseRedirect(next_page or request.path)
 
+
 def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
     """
     Logs out the user if he is logged in. Then redirects to the log-in page.
@@ -113,6 +114,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N
     login_url = resolve_url(login_url)
     return logout(request, login_url, current_app=current_app, extra_context=extra_context)
 
+
 def redirect_to_login(next, login_url=None,
                       redirect_field_name=REDIRECT_FIELD_NAME):
     """
@@ -128,6 +130,7 @@ def redirect_to_login(next, login_url=None,
 
     return HttpResponseRedirect(urlunparse(login_url_parts))
 
+
 # 4 views for password reset:
 # - password_reset sends the mail
 # - password_reset_done shows a success message for the above
@@ -173,6 +176,7 @@ def password_reset(request, is_admin_site=False,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 def password_reset_done(request,
                         template_name='registration/password_reset_done.html',
                         current_app=None, extra_context=None):
@@ -182,6 +186,7 @@ def password_reset_done(request,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 # Doesn't need csrf_protect since no-one can guess the URL
 @sensitive_post_parameters()
 @never_cache
@@ -195,13 +200,14 @@ def password_reset_confirm(request, uidb36=None, token=None,
     View that checks the hash in a password reset link and presents a
     form for entering a new password.
     """
-    assert uidb36 is not None and token is not None # checked by URLconf
+    UserModel = get_user_model()
+    assert uidb36 is not None and token is not None  # checked by URLconf
     if post_reset_redirect is None:
         post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
     try:
         uid_int = base36_to_int(uidb36)
-        user = User.objects.get(id=uid_int)
-    except (ValueError, OverflowError, User.DoesNotExist):
+        user = UserModel.objects.get(id=uid_int)
+    except (ValueError, OverflowError, UserModel.DoesNotExist):
         user = None
 
     if user is not None and token_generator.check_token(user, token):
@@ -225,6 +231,7 @@ def password_reset_confirm(request, uidb36=None, token=None,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 def password_reset_complete(request,
                             template_name='registration/password_reset_complete.html',
                             current_app=None, extra_context=None):
@@ -236,6 +243,7 @@ def password_reset_complete(request,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 @sensitive_post_parameters()
 @csrf_protect
 @login_required
@@ -261,6 +269,7 @@ def password_change(request,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+
 @login_required
 def password_change_done(request,
                          template_name='registration/password_change_done.html',

+ 11 - 6
django/contrib/comments/models.py

@@ -1,16 +1,16 @@
-from django.contrib.auth.models import User
+from django.conf import settings
 from django.contrib.comments.managers import CommentManager
 from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
-from django.db import models
 from django.core import urlresolvers
+from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.utils import timezone
-from django.conf import settings
 from django.utils.encoding import python_2_unicode_compatible
 
-COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+
 
 class BaseCommentAbstractModel(models.Model):
     """
@@ -40,6 +40,7 @@ class BaseCommentAbstractModel(models.Model):
             args=(self.content_type_id, self.object_pk)
         )
 
+
 @python_2_unicode_compatible
 class Comment(BaseCommentAbstractModel):
     """
@@ -49,7 +50,7 @@ class Comment(BaseCommentAbstractModel):
     # Who posted this comment? If ``user`` is set then it was an authenticated
     # user; otherwise at least user_name should have been set and the comment
     # was posted by a non-authenticated user.
-    user        = models.ForeignKey(User, verbose_name=_('user'),
+    user        = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
                     blank=True, null=True, related_name="%(class)s_comments")
     user_name   = models.CharField(_("user's name"), max_length=50, blank=True)
     user_email  = models.EmailField(_("user's email address"), blank=True)
@@ -117,6 +118,7 @@ class Comment(BaseCommentAbstractModel):
 
     def _get_name(self):
         return self.userinfo["name"]
+
     def _set_name(self, val):
         if self.user_id:
             raise AttributeError(_("This comment was posted by an authenticated "\
@@ -126,6 +128,7 @@ class Comment(BaseCommentAbstractModel):
 
     def _get_email(self):
         return self.userinfo["email"]
+
     def _set_email(self, val):
         if self.user_id:
             raise AttributeError(_("This comment was posted by an authenticated "\
@@ -135,6 +138,7 @@ class Comment(BaseCommentAbstractModel):
 
     def _get_url(self):
         return self.userinfo["url"]
+
     def _set_url(self, val):
         self.user_url = val
     url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
@@ -155,6 +159,7 @@ class Comment(BaseCommentAbstractModel):
         }
         return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
 
+
 @python_2_unicode_compatible
 class CommentFlag(models.Model):
     """
@@ -169,7 +174,7 @@ class CommentFlag(models.Model):
     design users are only allowed to flag a comment with a given flag once;
     if you want rating look elsewhere.
     """
-    user      = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags")
+    user      = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
     comment   = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
     flag      = models.CharField(_('flag'), max_length=30, db_index=True)
     flag_date = models.DateTimeField(_('date'), default=None)

+ 12 - 1
django/core/exceptions.py

@@ -3,42 +3,54 @@ Global Django exception and warning classes.
 """
 from functools import reduce
 
+
 class DjangoRuntimeWarning(RuntimeWarning):
     pass
 
+
 class ObjectDoesNotExist(Exception):
     "The requested object does not exist"
     silent_variable_failure = True
 
+
 class MultipleObjectsReturned(Exception):
     "The query returned multiple objects when only one was expected."
     pass
 
+
 class SuspiciousOperation(Exception):
     "The user did something suspicious"
     pass
 
+
 class PermissionDenied(Exception):
     "The user did not have permission to do that"
     pass
 
+
 class ViewDoesNotExist(Exception):
     "The requested view does not exist"
     pass
 
+
 class MiddlewareNotUsed(Exception):
     "This middleware is not used in this server configuration"
     pass
 
+
 class ImproperlyConfigured(Exception):
     "Django is somehow improperly configured"
     pass
 
+
 class FieldError(Exception):
     """Some kind of problem with a model field."""
     pass
 
+
 NON_FIELD_ERRORS = '__all__'
+
+
 class ValidationError(Exception):
     """An error while validating data."""
     def __init__(self, message, code=None, params=None):
@@ -85,4 +97,3 @@ class ValidationError(Exception):
         else:
             error_dict[NON_FIELD_ERRORS] = self.messages
         return error_dict
-

+ 1 - 0
django/core/management/commands/sqlall.py

@@ -6,6 +6,7 @@ from django.core.management.base import AppCommand
 from django.core.management.sql import sql_all
 from django.db import connections, DEFAULT_DB_ALIAS
 
+
 class Command(AppCommand):
     help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)."
 

+ 1 - 1
django/core/management/commands/syncdb.py

@@ -68,6 +68,7 @@ class Command(NoArgsCommand):
                 if router.allow_syncdb(db, m)])
             for app in models.get_apps()
         ]
+
         def model_installed(model):
             opts = model._meta
             converter = connection.introspection.table_name_converter
@@ -101,7 +102,6 @@ class Command(NoArgsCommand):
                     cursor.execute(statement)
                 tables.append(connection.introspection.table_name_converter(model._meta.db_table))
 
-
         transaction.commit_unless_managed(using=db)
 
         # Send the post_syncdb signal, so individual apps can do whatever they need

+ 1 - 0
django/core/management/commands/validate.py

@@ -1,5 +1,6 @@
 from django.core.management.base import NoArgsCommand
 
+
 class Command(NoArgsCommand):
     help = "Validates all installed models."
 

+ 10 - 2
django/core/management/sql.py

@@ -9,6 +9,7 @@ from django.core.management.base import CommandError
 from django.db import models
 from django.db.models import get_models
 
+
 def sql_create(app, style, connection):
     "Returns a list of the CREATE TABLE SQL statements for the given app."
 
@@ -55,6 +56,7 @@ def sql_create(app, style, connection):
 
     return final_output
 
+
 def sql_delete(app, style, connection):
     "Returns a list of the DROP TABLE SQL statements for the given app."
 
@@ -83,7 +85,7 @@ def sql_delete(app, style, connection):
             opts = model._meta
             for f in opts.local_fields:
                 if f.rel and f.rel.to not in to_delete:
-                    references_to_delete.setdefault(f.rel.to, []).append( (model, f) )
+                    references_to_delete.setdefault(f.rel.to, []).append((model, f))
 
             to_delete.add(model)
 
@@ -97,7 +99,8 @@ def sql_delete(app, style, connection):
         cursor.close()
         connection.close()
 
-    return output[::-1] # Reverse it, to deal with table dependencies.
+    return output[::-1]  # Reverse it, to deal with table dependencies.
+
 
 def sql_flush(style, connection, only_django=False, reset_sequences=True):
     """
@@ -114,6 +117,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True):
     statements = connection.ops.sql_flush(style, tables, seqs)
     return statements
 
+
 def sql_custom(app, style, connection):
     "Returns a list of the custom table modifying SQL statements for the given app."
     output = []
@@ -125,6 +129,7 @@ def sql_custom(app, style, connection):
 
     return output
 
+
 def sql_indexes(app, style, connection):
     "Returns a list of the CREATE INDEX SQL statements for all models in the given app."
     output = []
@@ -132,10 +137,12 @@ def sql_indexes(app, style, connection):
         output.extend(connection.creation.sql_indexes_for_model(model, style))
     return output
 
+
 def sql_all(app, style, connection):
     "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module."
     return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection)
 
+
 def _split_statements(content):
     comment_re = re.compile(r"^((?:'[^']*'|[^'])*?)--.*$")
     statements = []
@@ -150,6 +157,7 @@ def _split_statements(content):
             statement = ""
     return statements
 
+
 def custom_sql_for_model(model, style, connection):
     opts = model._meta
     app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))

+ 24 - 3
django/core/management/validation.py

@@ -5,6 +5,7 @@ from django.utils.encoding import force_str
 from django.utils.itercompat import is_iterable
 from django.utils import six
 
+
 class ModelErrorCollection:
     def __init__(self, outfile=sys.stdout):
         self.errors = []
@@ -15,6 +16,7 @@ class ModelErrorCollection:
         self.errors.append((context, error))
         self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error))))
 
+
 def get_validation_errors(outfile, app=None):
     """
     Validates all models that are part of the specified app. If no app name is provided,
@@ -56,7 +58,7 @@ def get_validation_errors(outfile, app=None):
                     e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name)
             if isinstance(f, models.DecimalField):
                 decimalp_ok, mdigits_ok = False, False
-                decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.'
+                decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.'
                 try:
                     decimal_places = int(f.decimal_places)
                     if decimal_places < 0:
@@ -123,6 +125,10 @@ def get_validation_errors(outfile, app=None):
                 if isinstance(f.rel.to, six.string_types):
                     continue
 
+                # Make sure the model we're related hasn't been swapped out
+                if f.rel.to._meta.swapped:
+                    e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
+
                 # Make sure the related field specified by a ForeignKey is unique
                 if not f.rel.to._meta.get_field(f.rel.field_name).unique:
                     e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__))
@@ -165,6 +171,10 @@ def get_validation_errors(outfile, app=None):
                 if isinstance(f.rel.to, six.string_types):
                     continue
 
+            # Make sure the model we're related hasn't been swapped out
+            if f.rel.to._meta.swapped:
+                e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
+
             # Check that the field is not set to unique.  ManyToManyFields do not support unique.
             if f.unique:
                 e.add(opts, "ManyToManyFields cannot be unique.  Remove the unique argument on '%s'." % f.name)
@@ -176,7 +186,7 @@ def get_validation_errors(outfile, app=None):
                 seen_from, seen_to, seen_self = False, False, 0
                 for inter_field in f.rel.through._meta.fields:
                     rel_to = getattr(inter_field.rel, 'to', None)
-                    if from_model == to_model: # relation to self
+                    if from_model == to_model:  # relation to self
                         if rel_to == from_model:
                             seen_self += 1
                         if seen_self > 2:
@@ -275,10 +285,21 @@ def get_validation_errors(outfile, app=None):
                     if r.get_accessor_name() == rel_query_name:
                         e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
 
+        # Check swappable attribute.
+        if opts.swapped:
+            try:
+                app_label, model_name = opts.swapped.split('.')
+            except ValueError:
+                e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable)
+                continue
+            if not models.get_model(app_label, model_name):
+                e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped)
+
         # Check ordering attribute.
         if opts.ordering:
             for field_name in opts.ordering:
-                if field_name == '?': continue
+                if field_name == '?':
+                    continue
                 if field_name.startswith('-'):
                     field_name = field_name[1:]
                 if opts.order_with_respect_to and field_name == '_order':

+ 21 - 12
django/core/validators.py

@@ -15,6 +15,7 @@ from django.utils import six
 # These values, if given to validate(), will trigger the self.required check.
 EMPTY_VALUES = (None, '', [], (), {})
 
+
 class RegexValidator(object):
     regex = ''
     message = _('Enter a valid value.')
@@ -39,14 +40,15 @@ class RegexValidator(object):
         if not self.regex.search(force_text(value)):
             raise ValidationError(self.message, code=self.code)
 
+
 class URLValidator(RegexValidator):
     regex = re.compile(
-        r'^(?:http|ftp)s?://' # http:// or https://
-        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
-        r'localhost|' #localhost...
-        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
-        r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
-        r'(?::\d+)?' # optional port
+        r'^(?:http|ftp)s?://'  # http:// or https://
+        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
+        r'localhost|'  # localhost...
+        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
+        r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
+        r'(?::\d+)?'  # optional port
         r'(?:/?|[/?]\S+)$', re.IGNORECASE)
 
     def __call__(self, value):
@@ -58,8 +60,8 @@ class URLValidator(RegexValidator):
                 value = force_text(value)
                 scheme, netloc, path, query, fragment = urlsplit(value)
                 try:
-                    netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
-                except UnicodeError: # invalid domain part
+                    netloc = netloc.encode('idna').decode('ascii')  # IDN -> ACE
+                except UnicodeError:  # invalid domain part
                     raise e
                 url = urlunsplit((scheme, netloc, path, query, fragment))
                 super(URLValidator, self).__call__(url)
@@ -75,6 +77,7 @@ def validate_integer(value):
     except (ValueError, TypeError):
         raise ValidationError('')
 
+
 class EmailValidator(RegexValidator):
 
     def __call__(self, value):
@@ -106,10 +109,12 @@ validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of le
 ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
 validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid')
 
+
 def validate_ipv6_address(value):
     if not is_valid_ipv6_address(value):
         raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid')
 
+
 def validate_ipv46_address(value):
     try:
         validate_ipv4_address(value)
@@ -125,6 +130,7 @@ ip_address_validator_map = {
     'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')),
 }
 
+
 def ip_address_validators(protocol, unpack_ipv4):
     """
     Depending on the given parameters returns the appropriate validators for
@@ -147,7 +153,7 @@ validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_
 
 class BaseValidator(object):
     compare = lambda self, a, b: a is not b
-    clean   = lambda self, x: x
+    clean = lambda self, x: x
     message = _('Ensure this value is %(limit_value)s (it is %(show_value)s).')
     code = 'limit_value'
 
@@ -164,25 +170,28 @@ class BaseValidator(object):
                 params=params,
             )
 
+
 class MaxValueValidator(BaseValidator):
     compare = lambda self, a, b: a > b
     message = _('Ensure this value is less than or equal to %(limit_value)s.')
     code = 'max_value'
 
+
 class MinValueValidator(BaseValidator):
     compare = lambda self, a, b: a < b
     message = _('Ensure this value is greater than or equal to %(limit_value)s.')
     code = 'min_value'
 
+
 class MinLengthValidator(BaseValidator):
     compare = lambda self, a, b: a < b
-    clean   = lambda self, x: len(x)
+    clean = lambda self, x: len(x)
     message = _('Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).')
     code = 'min_length'
 
+
 class MaxLengthValidator(BaseValidator):
     compare = lambda self, a, b: a > b
-    clean   = lambda self, x: len(x)
+    clean = lambda self, x: len(x)
     message = _('Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).')
     code = 'max_length'
-

+ 8 - 2
django/db/backends/__init__.py

@@ -319,6 +319,7 @@ class BaseDatabaseWrapper(object):
     def make_debug_cursor(self, cursor):
         return util.CursorDebugWrapper(cursor, self)
 
+
 class BaseDatabaseFeatures(object):
     allows_group_by_pk = False
     # True if django.db.backend.utils.typecast_timestamp is used on values
@@ -776,7 +777,7 @@ class BaseDatabaseOperations(object):
         The `style` argument is a Style object as returned by either
         color_style() or no_style() in django.core.management.color.
         """
-        return [] # No sequence reset required by default.
+        return []  # No sequence reset required by default.
 
     def start_transaction_sql(self):
         """
@@ -915,6 +916,7 @@ class BaseDatabaseOperations(object):
         conn = ' %s ' % connector
         return conn.join(sub_expressions)
 
+
 class BaseDatabaseIntrospection(object):
     """
     This class encapsulates all backend-specific introspection utilities
@@ -1010,12 +1012,14 @@ class BaseDatabaseIntrospection(object):
             for model in models.get_models(app):
                 if not model._meta.managed:
                     continue
+                if model._meta.swapped:
+                    continue
                 if not router.allow_syncdb(self.connection.alias, model):
                     continue
                 for f in model._meta.local_fields:
                     if isinstance(f, models.AutoField):
                         sequence_list.append({'table': model._meta.db_table, 'column': f.column})
-                        break # Only one AutoField is allowed per model, so don't bother continuing.
+                        break  # Only one AutoField is allowed per model, so don't bother continuing.
 
                 for f in model._meta.local_many_to_many:
                     # If this is an m2m using an intermediate table,
@@ -1052,6 +1056,7 @@ class BaseDatabaseIntrospection(object):
         """
         raise NotImplementedError
 
+
 class BaseDatabaseClient(object):
     """
     This class encapsulates all backend-specific methods for opening a
@@ -1068,6 +1073,7 @@ class BaseDatabaseClient(object):
     def runshell(self):
         raise NotImplementedError()
 
+
 class BaseDatabaseValidation(object):
     """
     This class encapsualtes all backend-specific model validation.

+ 8 - 8
django/db/backends/creation.py

@@ -40,7 +40,7 @@ class BaseDatabaseCreation(object):
             (list_of_sql, pending_references_dict)
         """
         opts = model._meta
-        if not opts.managed or opts.proxy:
+        if not opts.managed or opts.proxy or opts.swapped:
             return [], {}
         final_output = []
         table_output = []
@@ -92,9 +92,9 @@ class BaseDatabaseCreation(object):
 
         full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' +
                           style.SQL_TABLE(qn(opts.db_table)) + ' (']
-        for i, line in enumerate(table_output): # Combine and add commas.
+        for i, line in enumerate(table_output):  # Combine and add commas.
             full_statement.append(
-                '    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
+                '    %s%s' % (line, i < len(table_output) - 1 and ',' or ''))
         full_statement.append(')')
         if opts.db_tablespace:
             tablespace_sql = self.connection.ops.tablespace_sql(
@@ -143,11 +143,11 @@ class BaseDatabaseCreation(object):
         """
         from django.db.backends.util import truncate_name
 
-        if not model._meta.managed or model._meta.proxy:
+        opts = model._meta
+        if not opts.managed or opts.proxy or opts.swapped:
             return []
         qn = self.connection.ops.quote_name
         final_output = []
-        opts = model._meta
         if model in pending_references:
             for rel_class, f in pending_references[model]:
                 rel_opts = rel_class._meta
@@ -172,7 +172,7 @@ class BaseDatabaseCreation(object):
         """
         Returns the CREATE INDEX SQL statements for a single model.
         """
-        if not model._meta.managed or model._meta.proxy:
+        if not model._meta.managed or model._meta.proxy or model._meta.swapped:
             return []
         output = []
         for f in model._meta.local_fields:
@@ -211,7 +211,7 @@ class BaseDatabaseCreation(object):
         Return the DROP TABLE and restraint dropping statements for a single
         model.
         """
-        if not model._meta.managed or model._meta.proxy:
+        if not model._meta.managed or model._meta.proxy or model._meta.swapped:
             return []
         # Drop the table now
         qn = self.connection.ops.quote_name
@@ -228,7 +228,7 @@ class BaseDatabaseCreation(object):
 
     def sql_remove_table_constraints(self, model, references_to_delete, style):
         from django.db.backends.util import truncate_name
-        if not model._meta.managed or model._meta.proxy:
+        if not model._meta.managed or model._meta.proxy or model._meta.swapped:
             return []
         output = []
         qn = self.connection.ops.quote_name

+ 14 - 6
django/db/models/base.py

@@ -5,7 +5,7 @@ import sys
 from functools import update_wrapper
 from django.utils.six.moves import zip
 
-import django.db.models.manager     # Imported to register signal handler.
+import django.db.models.manager  # Imported to register signal handler.
 from django.conf import settings
 from django.core.exceptions import (ObjectDoesNotExist,
     MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS)
@@ -108,6 +108,11 @@ class ModelBase(type):
 
         is_proxy = new_class._meta.proxy
 
+        # If the model is a proxy, ensure that the base class
+        # hasn't been swapped out.
+        if is_proxy and base_meta and base_meta.swapped:
+            raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped))
+
         if getattr(new_class, '_default_manager', None):
             if not is_proxy:
                 # Multi-table inheritance doesn't inherit default manager from
@@ -262,6 +267,7 @@ class ModelBase(type):
         if opts.order_with_respect_to:
             cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True)
             cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False)
+
             # defer creating accessors on the foreign class until we are
             # certain it has been created
             def make_foreign_order_accessors(field, model, cls):
@@ -292,6 +298,7 @@ class ModelBase(type):
 
         signals.class_prepared.send(sender=cls)
 
+
 class ModelState(object):
     """
     A class for storing instance state
@@ -303,6 +310,7 @@ class ModelState(object):
         # This impacts validation only; it has no effect on the actual save.
         self.adding = True
 
+
 class Model(six.with_metaclass(ModelBase, object)):
     _deferred = False
 
@@ -632,7 +640,6 @@ class Model(six.with_metaclass(ModelBase, object)):
             signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
                                    update_fields=update_fields, raw=raw, using=using)
 
-
     save_base.alters_data = True
 
     def delete(self, using=None):
@@ -656,7 +663,7 @@ class Model(six.with_metaclass(ModelBase, object)):
         order = not is_next and '-' or ''
         param = force_text(getattr(self, field.attname))
         q = Q(**{'%s__%s' % (field.name, op): param})
-        q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk})
+        q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk})
         qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
         try:
             return qs[0]
@@ -849,7 +856,7 @@ class Model(six.with_metaclass(ModelBase, object)):
             field = opts.get_field(field_name)
             field_label = capfirst(field.verbose_name)
             # Insert the error into the error dict, very sneaky
-            return field.error_messages['unique'] %  {
+            return field.error_messages['unique'] % {
                 'model_name': six.text_type(model_name),
                 'field_label': six.text_type(field_label)
             }
@@ -857,7 +864,7 @@ class Model(six.with_metaclass(ModelBase, object)):
         else:
             field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check]
             field_labels = get_text_list(field_labels, _('and'))
-            return _("%(model_name)s with this %(field_label)s already exists.") %  {
+            return _("%(model_name)s with this %(field_label)s already exists.") % {
                 'model_name': six.text_type(model_name),
                 'field_label': six.text_type(field_labels)
             }
@@ -921,7 +928,6 @@ class Model(six.with_metaclass(ModelBase, object)):
             raise ValidationError(errors)
 
 
-
 ############################################
 # HELPER FUNCTIONS (CURRIED MODEL METHODS) #
 ############################################
@@ -963,6 +969,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
 class Empty(object):
     pass
 
+
 def model_unpickle(model, attrs):
     """
     Used to unpickle Model subclasses with deferred fields.
@@ -971,6 +978,7 @@ def model_unpickle(model, attrs):
     return cls.__new__(cls)
 model_unpickle.__safe_for_unpickle__ = True
 
+
 def unpickle_inner_exception(klass, exception_name):
     # Get the exception class from the class it is attached to:
     exception = getattr(klass, exception_name)

+ 27 - 8
django/db/models/fields/related.py

@@ -21,6 +21,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
 
 pending_lookups = {}
 
+
 def add_lazy_relation(cls, field, relation, operation):
     """
     Adds a lookup on ``cls`` when a related field is defined using a string,
@@ -77,6 +78,7 @@ def add_lazy_relation(cls, field, relation, operation):
         value = (cls, field, operation)
         pending_lookups.setdefault(key, []).append(value)
 
+
 def do_pending_lookups(sender, **kwargs):
     """
     Handle any pending relations to the sending model. Sent from class_prepared.
@@ -87,6 +89,7 @@ def do_pending_lookups(sender, **kwargs):
 
 signals.class_prepared.connect(do_pending_lookups)
 
+
 #HACK
 class RelatedField(object):
     def contribute_to_class(self, cls, name):
@@ -220,6 +223,7 @@ class RelatedField(object):
         # "related_name" option.
         return self.rel.related_name or self.opts.object_name.lower()
 
+
 class SingleRelatedObjectDescriptor(object):
     # This class provides the functionality that makes the related-object
     # managers available as attributes on a model class, for fields that have
@@ -306,6 +310,7 @@ class SingleRelatedObjectDescriptor(object):
         setattr(instance, self.cache_name, value)
         setattr(value, self.related.field.get_cache_name(), instance)
 
+
 class ReverseSingleRelatedObjectDescriptor(object):
     # This class provides the functionality that makes the related-object
     # managers available as attributes on a model class, for fields that have
@@ -430,6 +435,7 @@ class ReverseSingleRelatedObjectDescriptor(object):
         if value is not None and not self.field.rel.multiple:
             setattr(value, self.field.related.get_cache_name(), instance)
 
+
 class ForeignRelatedObjectsDescriptor(object):
     # This class provides the functionality that makes the related-object
     # managers available as attributes on a model class, for fields that have
@@ -660,7 +666,7 @@ def create_many_related_manager(superclass, rel):
                 for obj in objs:
                     if isinstance(obj, self.model):
                         if not router.allow_relation(obj, self.instance):
-                           raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
+                            raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
                                                (obj, self.instance._state.db, obj._state.db))
                         new_ids.add(obj.pk)
                     elif isinstance(obj, Model):
@@ -752,6 +758,7 @@ def create_many_related_manager(superclass, rel):
 
     return ManyRelatedManager
 
+
 class ManyRelatedObjectsDescriptor(object):
     # This class provides the functionality that makes the related-object
     # managers available as attributes on a model class, for fields that have
@@ -860,12 +867,13 @@ class ReverseManyRelatedObjectsDescriptor(object):
         manager.clear()
         manager.add(*value)
 
+
 class ManyToOneRel(object):
     def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
         parent_link=False, on_delete=None):
         try:
             to._meta
-        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
+        except AttributeError:  # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
             assert isinstance(to, six.string_types), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT
         self.to, self.field_name = to, field_name
         self.related_name = related_name
@@ -891,6 +899,7 @@ class ManyToOneRel(object):
                     self.field_name)
         return data[0]
 
+
 class OneToOneRel(ManyToOneRel):
     def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
         parent_link=False, on_delete=None):
@@ -900,6 +909,7 @@ class OneToOneRel(ManyToOneRel):
         )
         self.multiple = False
 
+
 class ManyToManyRel(object):
     def __init__(self, to, related_name=None, limit_choices_to=None,
             symmetrical=True, through=None):
@@ -924,16 +934,18 @@ class ManyToManyRel(object):
         """
         return self.to._meta.pk
 
+
 class ForeignKey(RelatedField, Field):
     empty_strings_allowed = False
     default_error_messages = {
         'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
     }
     description = _("Foreign Key (type determined by related field)")
+
     def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
         try:
             to_name = to._meta.object_name.lower()
-        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
+        except AttributeError:  # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
             assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
         else:
             assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
@@ -1050,6 +1062,7 @@ class ForeignKey(RelatedField, Field):
             return IntegerField().db_type(connection=connection)
         return rel_field.db_type(connection=connection)
 
+
 class OneToOneField(ForeignKey):
     """
     A OneToOneField is essentially the same as a ForeignKey, with the exception
@@ -1058,6 +1071,7 @@ class OneToOneField(ForeignKey):
     rather than returning a list.
     """
     description = _("One-to-one relationship")
+
     def __init__(self, to, to_field=None, **kwargs):
         kwargs['unique'] = True
         super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@@ -1077,12 +1091,14 @@ class OneToOneField(ForeignKey):
         else:
             setattr(instance, self.attname, data)
 
+
 def create_many_to_many_intermediary_model(field, klass):
     from django.db import models
     managed = True
     if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
         to_model = field.rel.to
         to = to_model.split('.')[-1]
+
         def set_managed(field, model, cls):
             field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
         add_lazy_relation(klass, field, to_model, set_managed)
@@ -1119,12 +1135,14 @@ def create_many_to_many_intermediary_model(field, klass):
         to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace)
     })
 
+
 class ManyToManyField(RelatedField, Field):
     description = _("Many-to-many relationship")
+
     def __init__(self, to, **kwargs):
         try:
             assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
-        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
+        except AttributeError:  # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
             assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
             # Python 2.6 and earlier require dictionary keys to be of str type,
             # not unicode and class names must be ASCII (in Python 2.x), so we
@@ -1135,7 +1153,7 @@ class ManyToManyField(RelatedField, Field):
         kwargs['rel'] = ManyToManyRel(to,
             related_name=kwargs.pop('related_name', None),
             limit_choices_to=kwargs.pop('limit_choices_to', None),
-            symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT),
+            symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT),
             through=kwargs.pop('through', None))
 
         self.db_table = kwargs.pop('db_table', None)
@@ -1166,7 +1184,7 @@ class ManyToManyField(RelatedField, Field):
         if hasattr(self, cache_attr):
             return getattr(self, cache_attr)
         for f in self.rel.through._meta.fields:
-            if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
+            if hasattr(f, 'rel') and f.rel and f.rel.to == related.model:
                 setattr(self, cache_attr, getattr(f, attr))
                 return getattr(self, cache_attr)
 
@@ -1177,7 +1195,7 @@ class ManyToManyField(RelatedField, Field):
             return getattr(self, cache_attr)
         found = False
         for f in self.rel.through._meta.fields:
-            if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
+            if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model:
                 if related.model == related.parent_model:
                     # If this is an m2m-intermediate to self,
                     # the first foreign key you find will be
@@ -1222,7 +1240,8 @@ class ManyToManyField(RelatedField, Field):
         # The intermediate m2m model is not auto created if:
         #  1) There is a manually specified intermediate, or
         #  2) The class owning the m2m field is abstract.
-        if not self.rel.through and not cls._meta.abstract:
+        #  3) The class owning the m2m field has been swapped out.
+        if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped:
             self.rel.through = create_many_to_many_intermediary_model(self, cls)
 
         # Add the descriptor for the m2m relation

+ 1 - 0
django/db/models/loading.py

@@ -14,6 +14,7 @@ import os
 __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
         'load_app', 'app_cache_ready')
 
+
 class AppCache(object):
     """
     A cache that stores installed applications and their models. Used to

+ 7 - 2
django/db/models/manager.py

@@ -13,7 +13,7 @@ def ensure_default_manager(sender, **kwargs):
     _default_manager if it's not a subclass of Manager).
     """
     cls = sender
-    if cls._meta.abstract:
+    if cls._meta.abstract or cls._meta.swapped:
         return
     if not getattr(cls, '_default_manager', None):
         # Create the default manager, if needed.
@@ -42,6 +42,7 @@ def ensure_default_manager(sender, **kwargs):
 
 signals.class_prepared.connect(ensure_default_manager)
 
+
 class Manager(object):
     # Tracks each time a Manager instance is created. Used to retain order.
     creation_counter = 0
@@ -56,7 +57,9 @@ class Manager(object):
     def contribute_to_class(self, model, name):
         # TODO: Use weakref because of possible memory leak / circular reference.
         self.model = model
-        setattr(model, name, ManagerDescriptor(self))
+        # Only contribute the manager if the model is concrete
+        if not model._meta.abstract and not model._meta.swapped:
+            setattr(model, name, ManagerDescriptor(self))
         if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter:
             model._default_manager = self
         if model._meta.abstract or (self._inherited and not self.model._meta.proxy):
@@ -208,6 +211,7 @@ class Manager(object):
     def raw(self, raw_query, params=None, *args, **kwargs):
         return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs)
 
+
 class ManagerDescriptor(object):
     # This class ensures managers aren't accessible via model instances.
     # For example, Poll.objects works, but poll_obj.objects raises AttributeError.
@@ -219,6 +223,7 @@ class ManagerDescriptor(object):
             raise AttributeError("Manager isn't accessible via %s instances" % type.__name__)
         return self.manager
 
+
 class EmptyManager(Manager):
     def get_query_set(self):
         return self.get_empty_query_set()

+ 18 - 3
django/db/models/options.py

@@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
 DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'unique_together', 'permissions', 'get_latest_by',
                  'order_with_respect_to', 'app_label', 'db_tablespace',
-                 'abstract', 'managed', 'proxy', 'auto_created')
+                 'abstract', 'managed', 'proxy', 'swappable', 'auto_created')
+
 
 @python_2_unicode_compatible
 class Options(object):
@@ -32,8 +33,8 @@ class Options(object):
         self.verbose_name_plural = None
         self.db_table = ''
         self.ordering = []
-        self.unique_together =  []
-        self.permissions =  []
+        self.unique_together = []
+        self.permissions = []
         self.object_name, self.app_label = None, app_label
         self.get_latest_by = None
         self.order_with_respect_to = None
@@ -55,6 +56,7 @@ class Options(object):
         # in the end of the proxy_for_model chain. In particular, for
         # concrete models, the concrete_model is always the class itself.
         self.concrete_model = None
+        self.swappable = None
         self.parents = SortedDict()
         self.duplicate_targets = {}
         self.auto_created = False
@@ -218,6 +220,19 @@ class Options(object):
         return raw
     verbose_name_raw = property(verbose_name_raw)
 
+    def _swapped(self):
+        """
+        Has this model been swapped out for another? If so, return the model
+        name of the replacement; otherwise, return None.
+        """
+        if self.swappable:
+            model_label = '%s.%s' % (self.app_label, self.object_name)
+            swapped_for = getattr(settings, self.swappable, None)
+            if swapped_for not in (None, model_label):
+                return swapped_for
+        return None
+    swapped = property(_swapped)
+
     def _fields(self):
         """
         The getter for self.fields. This returns the list of field objects

+ 2 - 1
django/test/__init__.py

@@ -5,5 +5,6 @@ Django Unit Test and Doctest framework.
 from django.test.client import Client, RequestFactory
 from django.test.testcases import (TestCase, TransactionTestCase,
     SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
-    skipUnlessDBFeature)
+    skipUnlessDBFeature
+)
 from django.test.utils import Approximate

+ 2 - 0
django/test/testcases.py

@@ -44,6 +44,7 @@ from django.utils import unittest as ut2
 from django.utils.encoding import force_text
 from django.utils import six
 from django.utils.unittest.util import safe_repr
+from django.utils.unittest import skipIf
 from django.views.static import serve
 
 __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
@@ -53,6 +54,7 @@ normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
 normalize_decimals = lambda s: re.sub(r"Decimal\('(\d+(\.\d*)?)'\)",
                                 lambda m: "Decimal(\"%s\")" % m.groups()[0], s)
 
+
 def to_list(value):
     """
     Puts value into a list if it's not already one.

+ 5 - 2
django/test/utils.py

@@ -28,7 +28,7 @@ class Approximate(object):
     def __eq__(self, other):
         if self.val == other:
             return True
-        return round(abs(self.val-other), self.places) == 0
+        return round(abs(self.val - other), self.places) == 0
 
 
 class ContextList(list):
@@ -46,7 +46,7 @@ class ContextList(list):
 
     def __contains__(self, key):
         try:
-            value = self[key]
+            self[key]
         except KeyError:
             return False
         return True
@@ -188,9 +188,11 @@ class override_settings(object):
         if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase):
             original_pre_setup = test_func._pre_setup
             original_post_teardown = test_func._post_teardown
+
             def _pre_setup(innerself):
                 self.enable()
                 original_pre_setup(innerself)
+
             def _post_teardown(innerself):
                 original_post_teardown(innerself)
                 self.disable()
@@ -220,5 +222,6 @@ class override_settings(object):
             setting_changed.send(sender=settings._wrapped.__class__,
                                  setting=key, value=new_value)
 
+
 def str_prefix(s):
     return s % {'_': '' if six.PY3 else 'u'}

+ 3 - 0
docs/internals/deprecation.txt

@@ -286,6 +286,9 @@ these changes.
 * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__``
   will be removed (``content_type`` should be used instead).
 
+* The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on
+  the User model, will be removed.
+
 2.0
 ---
 

+ 21 - 6
docs/ref/settings.txt

@@ -110,15 +110,14 @@ A tuple of authentication backend classes (as strings) to use when attempting to
 authenticate a user. See the :doc:`authentication backends documentation
 </ref/authbackends>` for details.
 
-.. setting:: AUTH_PROFILE_MODULE
+.. setting:: AUTH_USER_MODEL
 
-AUTH_PROFILE_MODULE
--------------------
+AUTH_USER_MODEL
+---------------
 
-Default: Not defined
+Default: 'auth.User'
 
-The site-specific user profile model used by this site. See
-:ref:`auth-profiles`.
+The model to use to represent a User. See :ref:`auth-custom-user`.
 
 .. setting:: CACHES
 
@@ -2209,6 +2208,22 @@ ADMIN_MEDIA_PREFIX
    integration. See the :doc:`Django 1.4 release notes</releases/1.4>` for
    more information.
 
+.. setting:: AUTH_PROFILE_MODULE
+
+AUTH_PROFILE_MODULE
+-------------------
+
+.. deprecated:: 1.5
+    With the introduction of :ref:`custom User models <auth-custom-user>`,
+    the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile
+    model is no longer supported. See the
+    :doc:`Django 1.5 release notes</releases/1.5>` for more information.
+
+Default: Not defined
+
+The site-specific user profile model used by this site. See
+:ref:`auth-profiles`.
+
 .. setting:: IGNORABLE_404_ENDS
 
 IGNORABLE_404_ENDS

+ 42 - 0
docs/releases/1.5.txt

@@ -34,6 +34,23 @@ release featuring 2.7 support.
 What's new in Django 1.5
 ========================
 
+Configurable User model
+~~~~~~~~~~~~~~~~~~~~~~~
+
+In Django 1.5, you can now use your own model as the store for user-related
+data. If your project needs a username with more than 30 characters, or if
+you want to store usernames in a format other than first name/last name, or
+you want to put custom profile information onto your User object, you can
+now do so.
+
+If you have a third-party reusable application that references the User model,
+you may need to make some changes to the way you reference User instances. You
+should also document any specific features of the User model that your
+application relies upon.
+
+See the :ref:`documentation on custom User models <auth-custom-user>` for
+more details.
+
 Support for saving a subset of model's fields
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -277,6 +294,18 @@ Session not saved on 500 responses
 Django's session middleware will skip saving the session data if the
 response's status code is 500.
 
+Email checks on failed admin login
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to Django 1.5, if you attempted to log into the admin interface and
+mistakenly used your email address instead of your username, the admin
+interface would provide a warning advising that your email address was
+not your username. In Django 1.5, the introduction of
+:ref:`custom User models <auth-custom-user>` has required the removal of this
+warning. This doesn't change the login behavior of the admin site; it only
+affects the warning message that is displayed under one particular mode of
+login failure.
+
 Changes in tests execution
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -394,3 +423,16 @@ The markup contrib module has been deprecated and will follow an accelerated
 deprecation schedule. Direct use of python markup libraries or 3rd party tag
 libraries is preferred to Django maintaining this functionality in the
 framework.
+
+:setting:`AUTH_PROFILE_MODULE`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+With the introduction of :ref:`custom User models <auth-custom-user>`, there is
+no longer any need for a built-in mechanism to store user profile data.
+
+You can still define user profiles models that have a one-to-one relation with
+the User model - in fact, for many applications needing to associate data with
+a User account, this will be an appropriate design pattern to follow. However,
+the :setting:`AUTH_PROFILE_MODULE` setting, and the
+:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing
+the user profile model, should not be used any longer.

+ 359 - 0
docs/topics/auth.txt

@@ -250,6 +250,12 @@ Methods
 
     .. method:: models.User.get_profile()
 
+        .. deprecated:: 1.5
+            With the introduction of :ref:`custom User models <auth-custom-user>`,
+            the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile
+            model is no longer supported. See the
+            :doc:`Django 1.5 release notes</releases/1.5>` for more information.
+
         Returns a site-specific profile for this user. Raises
         :exc:`django.contrib.auth.models.SiteProfileNotAvailable` if the
         current site doesn't allow profiles, or
@@ -582,6 +588,12 @@ correct path and environment for you.
 Storing additional information about users
 ------------------------------------------
 
+.. deprecated:: 1.5
+    With the introduction of :ref:`custom User models <auth-custom-user>`,
+    the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile
+    model is no longer supported. See the
+    :doc:`Django 1.5 release notes</releases/1.5>` for more information.
+
 If you'd like to store additional information related to your users, Django
 provides a method to specify a site-specific related model -- termed a "user
 profile" -- for this purpose.
@@ -1345,6 +1357,9 @@ Helper functions
       URL to redirect to after log out. Overrides ``next`` if the given
       ``GET`` parameter is passed.
 
+
+.. _built-in-auth-forms:
+
 Built-in forms
 --------------
 
@@ -1735,6 +1750,350 @@ Fields
         group.permissions.remove(permission, permission, ...)
         group.permissions.clear()
 
+.. _auth-custom-user:
+
+Customizing the User model
+==========================
+
+.. versionadded:: 1.5
+
+Some kinds of projects may have authentication requirements for which Django's
+built-in :class:`~django.contrib.auth.models.User` model is not always
+appropriate. For instance, on some sites it makes more sense to use an email
+address as your identification token instead of a username.
+
+Django allows you to override the default User model by providing a value for
+the :setting:`AUTH_USER_MODEL` setting that references a custom model::
+
+     AUTH_USER_MODEL = 'myapp.MyUser'
+
+This dotted pair describes the name of the Django app, and the name of the Django
+model that you wish to use as your User model.
+
+.. admonition:: Warning
+
+   Changing :setting:`AUTH_USER_MODEL` has a big effect on your database
+   structure. It changes the tables that are available, and it will affect the
+   construction of foreign keys and many-to-many relationships. If you intend
+   to set :setting:`AUTH_USER_MODEL`, you should set it before running
+   ``manage.py syncdb`` for the first time.
+
+   If you have an existing project and you want to migrate to using a custom
+   User model, you may need to look into using a migration tool like South_
+   to ease the transition.
+
+.. _South: http://south.aeracode.org
+
+Referencing the User model
+--------------------------
+
+If you reference :class:`~django.contrib.auth.models.User` directly (for
+example, by referring to it in a foreign key), your code will not work in
+projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a
+different User model.
+
+Instead of referring to :class:`~django.contrib.auth.models.User` directly,
+you should reference the user model using
+:func:`django.contrib.auth.get_user_model()`. This method will return the
+currently  active User model -- the custom User model if one is specified, or
+:class:`~django.contrib.auth.User` otherwise.
+
+In relations to the User model, you should specify the custom model using
+the :setting:`AUTH_USER_MODEL` setting. For example::
+
+    from django.conf import settings
+    from django.db import models
+
+    class Article(models.Model)
+        author = models.ForeignKey(settings.AUTH_USER_MODEL)
+
+Specifying a custom User model
+------------------------------
+
+.. admonition:: Model design considerations
+
+    Think carefully before handling information not directly related to
+    authentication in your custom User Model.
+
+    It may be better to store app-specific user information in a model
+    that has a relation with the User model. That allows each app to specify
+    its own user data requirements without risking conflicts with other
+    apps. On the other hand, queries to retrieve this related information
+    will involve a database join, which may have an effect on performance.
+
+Django expects your custom User model to meet some minimum requirements.
+
+1. Your model must have a single unique field that can be used for
+   identification purposes. This can be a username, an email address,
+   or any other unique attribute.
+
+2. Your model must provide a way to address the user in a "short" and
+   "long" form. The most common interpretation of this would be to use
+   the user's given name as the "short" identifier, and the user's full
+   name as the "long" identifier. However, there are no constraints on
+   what these two methods return - if you want, they can return exactly
+   the same value.
+
+The easiest way to construct a compliant custom User model is to inherit from
+:class:`~django.contrib.auth.models.AbstractBaseUser`.
+:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core
+implementation of a `User` model, including hashed passwords and tokenized
+password resets. You must then provide some key implementation details:
+
+.. attribute:: User.USERNAME_FIELD
+
+    A string describing the name of the field on the User model that is
+    used as the unique identifier. This will usually be a username of
+    some kind, but it can also be an email address, or any other unique
+    identifier. In the following example, the field `identifier` is used
+    as the identifying field::
+
+        class MyUser(AbstractBaseUser):
+            identfier = models.CharField(max_length=40, unique=True, db_index=True)
+            ...
+            USERNAME_FIELD = 'identifier'
+
+.. attribute:: User.REQUIRED_FIELDS
+
+    A list of the field names that *must* be provided when creating
+    a user. For example, here is the partial definition for a User model
+    that defines two required fields - a date of birth and height::
+
+        class MyUser(AbstractBaseUser):
+            ...
+            date_of_birth = models.DateField()
+            height = models.FloatField()
+            ...
+            REQUIRED_FIELDS = ['date_of_birth', 'height']
+
+.. method:: User.get_full_name():
+
+    A longer formal identifier for the user. A common interpretation
+    would be the full name name of the user, but it can be any string that
+    identifies the user.
+
+.. method:: User.get_short_name():
+
+    A short, informal identifier for the user. A common interpretation
+    would be the first name of the user, but it can be any string that
+    identifies the user in an informal way. It may also return the same
+    value as :meth:`django.contrib.auth.User.get_full_name()`.
+
+You should also define a custom manager for your User model. If your User
+model defines `username` and `email` fields the same as Django's default User,
+you can just install Django's
+:class:`~django.contrib.auth.models.UserManager`; however, if your User model
+defines different fields, you will need to define a custom manager that
+extends :class:`~django.contrib.auth.models.BaseUserManager` providing two
+additional methods:
+
+.. method:: UserManager.create_user(username, password=None, **other_fields)
+
+    The prototype of `create_user()` should accept all required fields
+    as arguments. For example, if your user model defines `username`,
+    and `date_of_birth` as required fields, then create_user should be
+    defined as::
+
+        def create_user(self, username, date_of_birth, password=None):
+            # create user here
+
+.. method:: UserManager.create_superuser(username, password, **other_fields)
+
+    The prototype of `create_superuser()` should accept all required fields
+    as arguments. For example, if your user model defines `username`,
+    and `date_of_birth` as required fields, then create_user should be
+    defined as::
+
+        def create_superuser(self, username, date_of_birth, password):
+            # create superuser here
+
+    Unlike `create_user()`, `create_superuser()` *must* require the caller
+    to provider a password.
+
+Extending Django's default User
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you're entirely happy with Django's :class:`~django.contrib.auth.models.User`
+model and you just want to add some additional profile information, you can
+simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your
+custom profile fields.
+
+Custom users and the built-in auth forms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As you may expect, built-in Django's :ref:`forms <_built-in-auth-forms>`
+and :ref:`views <other-built-in-views>` make certain assumptions about
+the user model that they are working with.
+
+If your user model doesn't follow the same assumptions, it may be necessary to define
+a replacement form, and pass that form in as part of the configuration of the
+auth views.
+
+* :class:`~django.contrib.auth.forms.UserCreationForm`
+
+  Depends on the :class:`~django.contrib.auth.models.User` model.
+  Must be re-written for any custom user model.
+
+* :class:`~django.contrib.auth.forms.UserChangeForm`
+
+  Depends on the :class:`~django.contrib.auth.models.User` model.
+  Must be re-written for any custom user model.
+
+* :class:`~django.contrib.auth.forms.AuthenticationForm`
+
+  Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`,
+  and will adapt to use the field defined in `USERNAME_FIELD`.
+
+* :class:`~django.contrib.auth.forms.PasswordResetForm`
+
+  Assumes that the user model has an integer primary key, has a field named
+  `email` that can be used to identify the user, and a boolean field
+  named `is_active` to prevent password resets for inactive users.
+
+* :class:`~django.contrib.auth.forms.SetPasswordForm`
+
+  Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`
+
+* :class:`~django.contrib.auth.forms.PasswordChangeForm`
+
+  Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`
+
+* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm`
+
+  Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`
+
+
+Custom users and django.contrib.admin
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want your custom User model to also work with Admin, your User model must
+define some additional attributes and methods. These methods allow the admin to
+control access of the User to admin content:
+
+.. attribute:: User.is_staff
+
+    Returns True if the user is allowed to have access to the admin site.
+
+.. attribute:: User.is_active
+
+    Returns True if the user account is currently active.
+
+.. method:: User.has_perm(perm, obj=None):
+
+    Returns True if the user has the named permission. If `obj` is
+    provided, the permission needs to be checked against a specific object
+    instance.
+
+.. method:: User.has_module_perms(app_label):
+
+    Returns True if the user has permission to access models in
+    the given app.
+
+
+Custom users and Proxy models
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One limitation of custom User models is that installing a custom User model
+will break any proxy model extending :class:`~django.contrib.auth.models.User`.
+Proxy models must be based on a concrete base class; by defining a custom User
+model, you remove the ability of Django to reliably identify the base class.
+
+If your project uses proxy models, you must either modify the proxy to extend
+the User model that is currently in use in your project, or merge your proxy's
+behavior into your User subclass.
+
+A full example
+--------------
+
+Here is an example of a full models.py for an admin-compliant custom
+user app. This user model uses an email address as the username, and has a
+required date of birth; it provides no permission checking, beyond a simple
+`admin` flag on the user account. This model would be compatible with all
+the built-in auth forms and views, except for the User creation forms.
+
+This code would all live in a ``models.py`` file for a custom
+authentication app::
+
+    from django.db import models
+    from django.contrib.auth.models import (
+        BaseUserManager, AbstractBaseUser
+    )
+
+
+    class MyUserManager(BaseUserManager):
+        def create_user(self, email, date_of_birth, password=None):
+            """
+            Creates and saves a User with the given email, date of
+            birth and password.
+            """
+            if not email:
+                raise ValueError('Users must have an email address')
+
+            user = self.model(
+                email=MyUserManager.normalize_email(email),
+                date_of_birth=date_of_birth,
+            )
+
+            user.set_password(password)
+            user.save(using=self._db)
+            return user
+
+        def create_superuser(self, username, date_of_birth, password):
+            """
+            Creates and saves a superuser with the given email, date of
+            birth and password.
+            """
+            u = self.create_user(username,
+                            password=password,
+                            date_of_birth=date_of_birth
+                        )
+            u.is_admin = True
+            u.save(using=self._db)
+            return u
+
+
+    class MyUser(AbstractBaseUser):
+        email = models.EmailField(
+                            verbose_name='email address',
+                            max_length=255
+                        )
+        date_of_birth = models.DateField()
+        is_active = models.BooleanField(default=True)
+        is_admin = models.BooleanField(default=False)
+
+        objects = MyUserManager()
+
+        USERNAME_FIELD = 'email'
+        REQUIRED_FIELDS = ['date_of_birth']
+
+        def get_full_name(self):
+            # The user is identified by their email address
+            return self.email
+
+        def get_short_name(self):
+            # The user is identified by their email address
+            return self.email
+
+        def __unicode__(self):
+            return self.email
+
+        def has_perm(self, perm, obj=None):
+            "Does the user have a specific permission?"
+            # Simplest possible answer: Yes, always
+            return True
+
+        def has_module_perms(self, app_label):
+            "Does the user have permissions to view the app `app_label`?"
+            # Simplest possible answer: Yes, always
+            return True
+
+        @property
+        def is_staff(self):
+            "Is the user a member of staff?"
+            # Simplest possible answer: All admins are staff
+            return self.is_admin
+
+
 .. _authentication-backends:
 
 Other authentication sources

+ 106 - 4
tests/modeltests/invalid_models/invalid_models/models.py

@@ -21,11 +21,12 @@ class FieldErrors(models.Model):
     decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10)
     filefield = models.FileField()
     choices = models.CharField(max_length=10, choices='bad')
-    choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)])
+    choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)])
     index = models.CharField(max_length=10, db_index='bad')
     field_ = models.CharField(max_length=10)
     nullbool = models.BooleanField(null=True)
 
+
 class Target(models.Model):
     tgt_safe = models.CharField(max_length=10)
     clash1 = models.CharField(max_length=10)
@@ -33,12 +34,14 @@ class Target(models.Model):
 
     clash1_set = models.CharField(max_length=10)
 
+
 class Clash1(models.Model):
     src_safe = models.CharField(max_length=10)
 
     foreign = models.ForeignKey(Target)
     m2m = models.ManyToManyField(Target)
 
+
 class Clash2(models.Model):
     src_safe = models.CharField(max_length=10)
 
@@ -48,6 +51,7 @@ class Clash2(models.Model):
     m2m_1 = models.ManyToManyField(Target, related_name='id')
     m2m_2 = models.ManyToManyField(Target, related_name='src_safe')
 
+
 class Target2(models.Model):
     clash3 = models.CharField(max_length=10)
     foreign_tgt = models.ForeignKey(Target)
@@ -56,6 +60,7 @@ class Target2(models.Model):
     m2m_tgt = models.ManyToManyField(Target)
     clashm2m_set = models.ManyToManyField(Target)
 
+
 class Clash3(models.Model):
     src_safe = models.CharField(max_length=10)
 
@@ -65,12 +70,15 @@ class Clash3(models.Model):
     m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt')
     m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt')
 
+
 class ClashForeign(models.Model):
     foreign = models.ForeignKey(Target2)
 
+
 class ClashM2M(models.Model):
     m2m = models.ManyToManyField(Target2)
 
+
 class SelfClashForeign(models.Model):
     src_safe = models.CharField(max_length=10)
     selfclashforeign = models.CharField(max_length=10)
@@ -79,6 +87,7 @@ class SelfClashForeign(models.Model):
     foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id')
     foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe')
 
+
 class ValidM2M(models.Model):
     src_safe = models.CharField(max_length=10)
     validm2m = models.CharField(max_length=10)
@@ -94,6 +103,7 @@ class ValidM2M(models.Model):
     m2m_3 = models.ManyToManyField('self')
     m2m_4 = models.ManyToManyField('self')
 
+
 class SelfClashM2M(models.Model):
     src_safe = models.CharField(max_length=10)
     selfclashm2m = models.CharField(max_length=10)
@@ -108,120 +118,148 @@ class SelfClashM2M(models.Model):
     m2m_3 = models.ManyToManyField('self', symmetrical=False)
     m2m_4 = models.ManyToManyField('self', symmetrical=False)
 
+
 class Model(models.Model):
     "But it's valid to call a model Model."
-    year = models.PositiveIntegerField() #1960
-    make = models.CharField(max_length=10) #Aston Martin
-    name = models.CharField(max_length=10) #DB 4 GT
+    year = models.PositiveIntegerField()  # 1960
+    make = models.CharField(max_length=10)  # Aston Martin
+    name = models.CharField(max_length=10)  # DB 4 GT
+
 
 class Car(models.Model):
     colour = models.CharField(max_length=5)
     model = models.ForeignKey(Model)
 
+
 class MissingRelations(models.Model):
     rel1 = models.ForeignKey("Rel1")
     rel2 = models.ManyToManyField("Rel2")
 
+
 class MissingManualM2MModel(models.Model):
     name = models.CharField(max_length=5)
     missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
 
+
 class Person(models.Model):
     name = models.CharField(max_length=5)
 
+
 class Group(models.Model):
     name = models.CharField(max_length=5)
     primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
     secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
     tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
 
+
 class GroupTwo(models.Model):
     name = models.CharField(max_length=5)
     primary = models.ManyToManyField(Person, through="Membership")
     secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
 
+
 class Membership(models.Model):
     person = models.ForeignKey(Person)
     group = models.ForeignKey(Group)
     not_default_or_null = models.CharField(max_length=5)
 
+
 class MembershipMissingFK(models.Model):
     person = models.ForeignKey(Person)
 
+
 class PersonSelfRefM2M(models.Model):
     name = models.CharField(max_length=5)
     friends = models.ManyToManyField('self', through="Relationship")
     too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
 
+
 class PersonSelfRefM2MExplicit(models.Model):
     name = models.CharField(max_length=5)
     friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
 
+
 class Relationship(models.Model):
     first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
     second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
     date_added = models.DateTimeField()
 
+
 class ExplicitRelationship(models.Model):
     first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set")
     second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set")
     date_added = models.DateTimeField()
 
+
 class RelationshipTripleFK(models.Model):
     first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
     second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
     third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
     date_added = models.DateTimeField()
 
+
 class RelationshipDoubleFK(models.Model):
     first = models.ForeignKey(Person, related_name="first_related_name")
     second = models.ForeignKey(Person, related_name="second_related_name")
     third = models.ForeignKey(Group, related_name="rel_to_set")
     date_added = models.DateTimeField()
 
+
 class AbstractModel(models.Model):
     name = models.CharField(max_length=10)
+
     class Meta:
         abstract = True
 
+
 class AbstractRelationModel(models.Model):
     fk1 = models.ForeignKey('AbstractModel')
     fk2 = models.ManyToManyField('AbstractModel')
 
+
 class UniqueM2M(models.Model):
     """ Model to test for unique ManyToManyFields, which are invalid. """
     unique_people = models.ManyToManyField(Person, unique=True)
 
+
 class NonUniqueFKTarget1(models.Model):
     """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """
     tgt = models.ForeignKey('FKTarget', to_field='bad')
 
+
 class UniqueFKTarget1(models.Model):
     """ Model to test for unique FK target in yet-to-be-defined model: expect no error """
     tgt = models.ForeignKey('FKTarget', to_field='good')
 
+
 class FKTarget(models.Model):
     bad = models.IntegerField()
     good = models.IntegerField(unique=True)
 
+
 class NonUniqueFKTarget2(models.Model):
     """ Model to test for non-unique FK target in previously seen model: expect an error """
     tgt = models.ForeignKey(FKTarget, to_field='bad')
 
+
 class UniqueFKTarget2(models.Model):
     """ Model to test for unique FK target in previously seen model: expect no error """
     tgt = models.ForeignKey(FKTarget, to_field='good')
 
+
 class NonExistingOrderingWithSingleUnderscore(models.Model):
     class Meta:
         ordering = ("does_not_exist",)
 
+
 class InvalidSetNull(models.Model):
     fk = models.ForeignKey('self', on_delete=models.SET_NULL)
 
+
 class InvalidSetDefault(models.Model):
     fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
 
+
 class UnicodeForeignKeys(models.Model):
     """Foreign keys which can translate to ascii should be OK, but fail if
     they're not."""
@@ -232,9 +270,11 @@ class UnicodeForeignKeys(models.Model):
     # when adding the errors in core/management/validation.py
     #bad = models.ForeignKey('★')
 
+
 class PrimaryKeyNull(models.Model):
     my_pk_field = models.IntegerField(primary_key=True, null=True)
 
+
 class OrderByPKModel(models.Model):
     """
     Model to test that ordering by pk passes validation.
@@ -245,6 +285,62 @@ class OrderByPKModel(models.Model):
     class Meta:
         ordering = ('pk',)
 
+
+class SwappableModel(models.Model):
+    """A model that can be, but isn't swapped out.
+
+    References to this model *shoudln't* raise any validation error.
+    """
+    name = models.CharField(max_length=100)
+
+    class Meta:
+        swappable = 'TEST_SWAPPABLE_MODEL'
+
+
+class SwappedModel(models.Model):
+    """A model that is swapped out.
+
+    References to this model *should* raise a validation error.
+    Requires TEST_SWAPPED_MODEL to be defined in the test environment;
+    this is guaranteed by the test runner using @override_settings.
+    """
+    name = models.CharField(max_length=100)
+
+    class Meta:
+        swappable = 'TEST_SWAPPED_MODEL'
+
+
+class BadSwappableValue(models.Model):
+    """A model that can be swapped out; during testing, the swappable
+    value is not of the format app.model
+    """
+    name = models.CharField(max_length=100)
+
+    class Meta:
+        swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE'
+
+
+class BadSwappableModel(models.Model):
+    """A model that can be swapped out; during testing, the swappable
+    value references an unknown model.
+    """
+    name = models.CharField(max_length=100)
+
+    class Meta:
+        swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL'
+
+
+class HardReferenceModel(models.Model):
+    fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1')
+    fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2')
+    fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3')
+    fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4')
+    m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1')
+    m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2')
+    m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3')
+    m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')
+
+
 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
 invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
 invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
@@ -353,6 +449,12 @@ invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have
 invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist.
 invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null.
 invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value.
+invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
+invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
+invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
+invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
+invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'.
+invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract.
 """
 
 if not connection.features.interprets_empty_strings_as_nulls:

+ 11 - 2
tests/modeltests/invalid_models/tests.py

@@ -4,6 +4,7 @@ import sys
 from django.core.management.validation import get_validation_errors
 from django.db.models.loading import cache, load_app
 
+from django.test.utils import override_settings
 from django.utils import unittest
 from django.utils.six import StringIO
 
@@ -31,14 +32,22 @@ class InvalidModelTestCase(unittest.TestCase):
         cache._get_models_cache = {}
         sys.stdout = self.old_stdout
 
+    # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be
+    # set to *something* in order for the test to work. However, it's
+    # easier to set this up as an override than to require every developer
+    # to specify a value in their test settings.
+    @override_settings(
+        TEST_SWAPPED_MODEL='invalid_models.Target',
+        TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model',
+        TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target',
+    )
     def test_invalid_models(self):
-
         try:
             module = load_app("modeltests.invalid_models.invalid_models")
         except Exception:
             self.fail('Unable to load invalid model module')
 
-        count = get_validation_errors(self.stdout, module)
+        get_validation_errors(self.stdout, module)
         self.stdout.seek(0)
         error_log = self.stdout.read()
         actual = error_log.split('\n')

+ 36 - 2
tests/modeltests/proxy_models/tests.py

@@ -1,10 +1,13 @@
 from __future__ import absolute_import, unicode_literals
+import copy
 
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core import management
 from django.core.exceptions import FieldError
 from django.db import models, DEFAULT_DB_ALIAS
 from django.db.models import signals
+from django.db.models.loading import cache
 from django.test import TestCase
 
 
@@ -13,6 +16,7 @@ from .models import (MyPerson, Person, StatusPerson, LowerStatusPerson,
     Country, State, StateProxy, TrackerUser, BaseUser, Bug, ProxyTrackerUser,
     Improvement, ProxyProxyBug, ProxyBug, ProxyImprovement)
 
+
 class ProxyModelTests(TestCase):
     def test_same_manager_queries(self):
         """
@@ -91,7 +95,7 @@ class ProxyModelTests(TestCase):
         )
         self.assertRaises(Person.MultipleObjectsReturned,
             MyPersonProxy.objects.get,
-            id__lt=max_id+1
+            id__lt=max_id + 1
         )
         self.assertRaises(Person.DoesNotExist,
             StatusPerson.objects.get,
@@ -104,7 +108,7 @@ class ProxyModelTests(TestCase):
 
         self.assertRaises(Person.MultipleObjectsReturned,
             StatusPerson.objects.get,
-            id__lt=max_id+1
+            id__lt=max_id + 1
         )
 
     def test_abc(self):
@@ -138,10 +142,40 @@ class ProxyModelTests(TestCase):
         def build_new_fields():
             class NoNewFields(Person):
                 newfield = models.BooleanField()
+
                 class Meta:
                     proxy = True
         self.assertRaises(FieldError, build_new_fields)
 
+    def test_swappable(self):
+        try:
+            # This test adds dummy applications to the app cache. These
+            # need to be removed in order to prevent bad interactions
+            # with the flush operation in other tests.
+            old_app_models = copy.deepcopy(cache.app_models)
+            old_app_store = copy.deepcopy(cache.app_store)
+
+            settings.TEST_SWAPPABLE_MODEL = 'proxy_models.AlternateModel'
+
+            class SwappableModel(models.Model):
+
+                class Meta:
+                    swappable = 'TEST_SWAPPABLE_MODEL'
+
+            class AlternateModel(models.Model):
+                pass
+
+            # You can't proxy a swapped model
+            with self.assertRaises(TypeError):
+                class ProxyModel(SwappableModel):
+
+                    class Meta:
+                        proxy = True
+        finally:
+            del settings.TEST_SWAPPABLE_MODEL
+            cache.app_models = old_app_models
+            cache.app_store = old_app_store
+
     def test_myperson_manager(self):
         Person.objects.create(name="fred")
         Person.objects.create(name="wilma")

+ 57 - 45
tests/regressiontests/admin_views/tests.py

@@ -52,6 +52,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount,
 ERROR_MESSAGE = "Please enter the correct username and password \
 for a staff account. Note that both fields are case-sensitive."
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminViewBasicTest(TestCase):
     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml',
@@ -141,7 +142,7 @@ class AdminViewBasicTest(TestCase):
             "article_set-MAX_NUM_FORMS": "0",
         }
         response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
 
     def testPopupAddPost(self):
         """
@@ -205,7 +206,7 @@ class AdminViewBasicTest(TestCase):
         A smoke test to ensure POST on edit_view works.
         """
         response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
 
     def testEditSaveAs(self):
         """
@@ -221,7 +222,7 @@ class AdminViewBasicTest(TestCase):
             "article_set-5-section": "1",
         })
         response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
 
     def testChangeListSortingCallable(self):
         """
@@ -308,7 +309,7 @@ class AdminViewBasicTest(TestCase):
         self.assertContentBefore(response, link2, link1)
 
         # Test we can override with query string
-        response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'})
+        response = self.client.get('/test_admin/admin/admin_views/language/', {'o': '-1'})
         self.assertContentBefore(response, link1, link2)
 
     def testChangeListSortingOverrideModelAdmin(self):
@@ -358,13 +359,13 @@ class AdminViewBasicTest(TestCase):
         kinds of 'ordering' fields: field names, method on the model
         admin and model itself, and other callables. See #17252.
         """
-        models = [(AdminOrderedField,       'adminorderedfield'      ),
+        models = [(AdminOrderedField, 'adminorderedfield'),
                   (AdminOrderedModelMethod, 'adminorderedmodelmethod'),
                   (AdminOrderedAdminMethod, 'adminorderedadminmethod'),
-                  (AdminOrderedCallable,    'adminorderedcallable'   )]
+                  (AdminOrderedCallable, 'adminorderedcallable')]
         for model, url in models:
-            a1 = model.objects.create(stuff='The Last Item',   order=3)
-            a2 = model.objects.create(stuff='The First Item',  order=1)
+            a1 = model.objects.create(stuff='The Last Item', order=3)
+            a2 = model.objects.create(stuff='The First Item', order=1)
             a3 = model.objects.create(stuff='The Middle Item', order=2)
             response = self.client.get('/test_admin/admin/admin_views/%s/' % url, {})
             self.assertEqual(response.status_code, 200)
@@ -671,7 +672,6 @@ class AdminJavaScriptTest(TestCase):
             '<script type="text/javascript">document.getElementById("id_start_date_0").focus();</script>'
         )
 
-
     def test_js_minified_only_if_debug_is_false(self):
         """
         Ensure that the minified versions of the JS files are only used when
@@ -709,7 +709,7 @@ class AdminJavaScriptTest(TestCase):
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class SaveAsTests(TestCase):
     urls = "regressiontests.admin_views.urls"
-    fixtures = ['admin-views-users.xml','admin-views-person.xml']
+    fixtures = ['admin-views-users.xml', 'admin-views-person.xml']
 
     def setUp(self):
         self.client.login(username='super', password='secret')
@@ -719,7 +719,7 @@ class SaveAsTests(TestCase):
 
     def test_save_as_duplication(self):
         """Ensure save as actually creates a new person"""
-        post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42}
+        post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42}
         response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
         self.assertEqual(len(Person.objects.filter(name='John M')), 1)
         self.assertEqual(len(Person.objects.filter(id=1)), 1)
@@ -732,10 +732,11 @@ class SaveAsTests(TestCase):
         """
         response = self.client.get('/test_admin/admin/admin_views/person/1/')
         self.assertTrue(response.context['save_as'])
-        post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'}
+        post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'}
         response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
         self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/')
 
+
 class CustomModelAdminTest(AdminViewBasicTest):
     urls = "regressiontests.admin_views.urls"
     urlbit = "admin2"
@@ -791,11 +792,13 @@ class CustomModelAdminTest(AdminViewBasicTest):
         response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
         self.assertEqual(response.content, b"Django is a magical pony!")
 
+
 def get_perm(Model, perm):
     """Return the permission object, for the Model"""
     ct = ContentType.objects.get_for_model(Model)
     return Permission.objects.get(content_type=ct, codename=perm)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminViewPermissionsTest(TestCase):
     """Tests for Admin Views Permissions."""
@@ -898,7 +901,7 @@ class AdminViewPermissionsTest(TestCase):
         response = self.client.get('/test_admin/admin/')
         self.assertEqual(response.status_code, 200)
         login = self.client.post('/test_admin/admin/', self.super_email_login)
-        self.assertContains(login, "Your e-mail address is not your username")
+        self.assertContains(login, ERROR_MESSAGE)
         # only correct passwords get a username hint
         login = self.client.post('/test_admin/admin/', self.super_email_bad_login)
         self.assertContains(login, ERROR_MESSAGE)
@@ -959,7 +962,7 @@ class AdminViewPermissionsTest(TestCase):
     def testAddView(self):
         """Test add view restricts access and actually adds items."""
 
-        add_dict = {'title' : 'Døm ikke',
+        add_dict = {'title': 'Døm ikke',
                     'content': '<p>great article</p>',
                     'date_0': '2008-03-18', 'date_1': '10:54:39',
                     'section': 1}
@@ -1014,7 +1017,7 @@ class AdminViewPermissionsTest(TestCase):
     def testChangeView(self):
         """Change view should restrict access and allow users to edit items."""
 
-        change_dict = {'title' : 'Ikke fordømt',
+        change_dict = {'title': 'Ikke fordømt',
                        'content': '<p>edited article</p>',
                        'date_0': '2008-03-18', 'date_1': '10:54:39',
                        'section': 1}
@@ -1346,6 +1349,7 @@ class AdminViewDeletedObjectsTest(TestCase):
         response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3))
         self.assertContains(response, should_contain)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminViewStringPrimaryKeyTest(TestCase):
     urls = "regressiontests.admin_views.urls"
@@ -1400,7 +1404,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
         response = self.client.get('/test_admin/admin/')
         should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (escape(quote(self.pk)), escape(self.pk))
         self.assertContains(response, should_contain)
-        should_contain = "Model with string primary key" # capitalized in Recent Actions
+        should_contain = "Model with string primary key"  # capitalized in Recent Actions
         self.assertContains(response, should_contain)
         logentry = LogEntry.objects.get(content_type__name__iexact=should_contain)
         # http://code.djangoproject.com/ticket/10275
@@ -1522,7 +1526,7 @@ class SecureViewTests(TestCase):
 
     def test_secure_view_shows_login_if_not_logged_in(self):
         "Ensure that we see the login form"
-        response = self.client.get('/test_admin/admin/secure-view/' )
+        response = self.client.get('/test_admin/admin/secure-view/')
         self.assertTemplateUsed(response, 'admin/login.html')
 
     def test_secure_view_login_successfully_redirects_to_original_url(self):
@@ -1556,7 +1560,7 @@ class SecureViewTests(TestCase):
         response = self.client.get('/test_admin/admin/secure-view/')
         self.assertEqual(response.status_code, 200)
         login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login)
-        self.assertContains(login, "Your e-mail address is not your username")
+        self.assertContains(login, ERROR_MESSAGE)
         # only correct passwords get a username hint
         login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login)
         self.assertContains(login, ERROR_MESSAGE)
@@ -1626,6 +1630,7 @@ class SecureViewTests(TestCase):
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response['Location'], 'http://example.com/users/super/')
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminViewUnicodeTest(TestCase):
     urls = "regressiontests.admin_views.urls"
@@ -1668,7 +1673,7 @@ class AdminViewUnicodeTest(TestCase):
         }
 
         response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
 
     def testUnicodeDelete(self):
         """
@@ -2035,7 +2040,7 @@ class AdminViewListEditable(TestCase):
         story1 = Story.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...')
         story2 = Story.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...')
         response = self.client.get('/test_admin/admin/admin_views/story/')
-        self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table.
+        self.assertContains(response, 'id="id_form-0-id"', 1)  # Only one hidden field, in a separate place than the table.
         self.assertContains(response, 'id="id_form-1-id"', 1)
         self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True)
         self.assertContains(response, '<td>%d</td>' % story1.id, 1)
@@ -2051,7 +2056,7 @@ class AdminViewListEditable(TestCase):
         link1 = reverse('admin:admin_views_otherstory_change', args=(story1.pk,))
         link2 = reverse('admin:admin_views_otherstory_change', args=(story2.pk,))
         response = self.client.get('/test_admin/admin/admin_views/otherstory/')
-        self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table.
+        self.assertContains(response, 'id="id_form-0-id"', 1)  # Only one hidden field, in a separate place than the table.
         self.assertContains(response, 'id="id_form-1-id"', 1)
         self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True)
         self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link1, story1.id), 1)
@@ -2109,7 +2114,7 @@ class AdminSearchTest(TestCase):
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminInheritedInlinesTest(TestCase):
     urls = "regressiontests.admin_views.urls"
-    fixtures = ['admin-views-users.xml',]
+    fixtures = ['admin-views-users.xml']
 
     def setUp(self):
         self.client.login(username='super', password='secret')
@@ -2146,7 +2151,7 @@ class AdminInheritedInlinesTest(TestCase):
         }
 
         response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
         self.assertEqual(Persona.objects.count(), 1)
         self.assertEqual(FooAccount.objects.count(), 1)
         self.assertEqual(BarAccount.objects.count(), 1)
@@ -2193,6 +2198,7 @@ class AdminInheritedInlinesTest(TestCase):
         self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
         self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminActionsTest(TestCase):
     urls = "regressiontests.admin_views.urls"
@@ -2208,7 +2214,7 @@ class AdminActionsTest(TestCase):
         "Tests a custom action defined in a ModelAdmin method"
         action_data = {
             ACTION_CHECKBOX_NAME: [1],
-            'action' : 'mail_admin',
+            'action': 'mail_admin',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
@@ -2219,12 +2225,12 @@ class AdminActionsTest(TestCase):
         "Tests the default delete action defined as a ModelAdmin method"
         action_data = {
             ACTION_CHECKBOX_NAME: [1, 2],
-            'action' : 'delete_selected',
+            'action': 'delete_selected',
             'index': 0,
         }
         delete_confirmation_data = {
             ACTION_CHECKBOX_NAME: [1, 2],
-            'action' : 'delete_selected',
+            'action': 'delete_selected',
             'post': 'yes',
         }
         confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
@@ -2248,12 +2254,12 @@ class AdminActionsTest(TestCase):
         subscriber.save()
         action_data = {
             ACTION_CHECKBOX_NAME: [9999, 2],
-            'action' : 'delete_selected',
+            'action': 'delete_selected',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
         self.assertTemplateUsed(response, 'admin/delete_selected_confirmation.html')
-        self.assertContains(response, 'value="9999"') # Instead of 9,999
+        self.assertContains(response, 'value="9999"')  # Instead of 9,999
         self.assertContains(response, 'value="2"')
         settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR
         settings.USE_L10N = self.old_USE_L10N
@@ -2270,7 +2276,7 @@ class AdminActionsTest(TestCase):
 
         action_data = {
             ACTION_CHECKBOX_NAME: [q1.pk, q2.pk],
-            'action' : 'delete_selected',
+            'action': 'delete_selected',
             'index': 0,
         }
 
@@ -2284,7 +2290,7 @@ class AdminActionsTest(TestCase):
         "Tests a custom action defined in a function"
         action_data = {
             ACTION_CHECKBOX_NAME: [1],
-            'action' : 'external_mail',
+            'action': 'external_mail',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
@@ -2295,7 +2301,7 @@ class AdminActionsTest(TestCase):
         "Tests a custom action defined in a function"
         action_data = {
             ACTION_CHECKBOX_NAME: [1],
-            'action' : 'redirect_to',
+            'action': 'redirect_to',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
@@ -2309,7 +2315,7 @@ class AdminActionsTest(TestCase):
         """
         action_data = {
             ACTION_CHECKBOX_NAME: [1],
-            'action' : 'external_mail',
+            'action': 'external_mail',
             'index': 0,
         }
         url = '/test_admin/admin/admin_views/externalsubscriber/?o=1'
@@ -2374,7 +2380,7 @@ class AdminActionsTest(TestCase):
         """
         action_data = {
             ACTION_CHECKBOX_NAME: [],
-            'action' : 'delete_selected',
+            'action': 'delete_selected',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
@@ -2388,7 +2394,7 @@ class AdminActionsTest(TestCase):
         """
         action_data = {
             ACTION_CHECKBOX_NAME: [1, 2],
-            'action' : '',
+            'action': '',
             'index': 0,
         }
         response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
@@ -2432,7 +2438,7 @@ class TestCustomChangeList(TestCase):
         # Insert some data
         post_data = {"name": "First Gadget"}
         response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data)
-        self.assertEqual(response.status_code, 302) # redirect somewhere
+        self.assertEqual(response.status_code, 302)  # redirect somewhere
         # Hit the page once to get messages out of the queue message list
         response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit)
         # Ensure that that data is still not visible on the page
@@ -2460,6 +2466,7 @@ class TestInlineNotEditable(TestCase):
         response = self.client.get('/test_admin/admin/admin_views/parent/add/')
         self.assertEqual(response.status_code, 200)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminCustomQuerysetTest(TestCase):
     urls = "regressiontests.admin_views.urls"
@@ -2516,6 +2523,7 @@ class AdminCustomQuerysetTest(TestCase):
         # Message should contain non-ugly model name. Instance representation is set by model's __unicode__()
         self.assertContains(response, '<li class="info">The cover letter &quot;John Doe II&quot; was changed successfully.</li>', html=True)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminInlineFileUploadTest(TestCase):
     urls = "regressiontests.admin_views.urls"
@@ -2656,7 +2664,7 @@ class AdminInlineTests(TestCase):
 
         result = self.client.login(username='super', password='secret')
         self.assertEqual(result, True)
-        self.collector = Collector(pk=1,name='John Fowles')
+        self.collector = Collector(pk=1, name='John Fowles')
         self.collector.save()
 
     def tearDown(self):
@@ -2982,14 +2990,14 @@ class PrePopulatedTest(TestCase):
         self.assertNotContains(response, "field['dependency_ids'].push('#id_title');")
         self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
 
-    @override_settings(USE_THOUSAND_SEPARATOR = True, USE_L10N = True)
+    @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
     def test_prepopulated_maxlength_localized(self):
         """
         Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure
         that maxLength (in the JavaScript) is rendered without separators.
         """
         response = self.client.get('/test_admin/admin/admin_views/prepopulatedpostlargeslug/add/')
-        self.assertContains(response, "maxLength: 1000") # instead of 1,000
+        self.assertContains(response, "maxLength: 1000")  # instead of 1,000
 
 
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -3035,8 +3043,8 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase):
         self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-name').send_keys(' now you haVe anöther   sŤāÇkeð  inline with a very ... loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... ')
         slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug1').get_attribute('value')
         slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug2').get_attribute('value')
-        self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field
-        self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field
+        self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo')  # 50 characters maximum for slug1 field
+        self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo')  # 60 characters maximum for slug2 field
 
         # Tabular inlines ----------------------------------------------------
         # Initial inline
@@ -3087,7 +3095,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase):
             slug2='option-one-here-stacked-inline',
         )
         RelatedPrepopulated.objects.get(
-            name=' now you haVe anöther   sŤāÇkeð  inline with a very ... loooooooooooooooooo', # 75 characters in name field
+            name=' now you haVe anöther   sŤāÇkeð  inline with a very ... loooooooooooooooooo',  # 75 characters in name field
             pubdate='1999-01-25',
             status='option two',
             slug1='now-you-have-another-stacked-inline-very-loooooooo',
@@ -3112,6 +3120,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase):
 class SeleniumPrePopulatedChromeTests(SeleniumPrePopulatedFirefoxTests):
     webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
 
+
 class SeleniumPrePopulatedIETests(SeleniumPrePopulatedFirefoxTests):
     webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
 
@@ -3172,7 +3181,7 @@ class ReadonlyTest(TestCase):
         p = Post.objects.get()
         self.assertEqual(p.posted, datetime.date.today())
 
-        data["posted"] = "10-8-1990" # some date that's not today
+        data["posted"] = "10-8-1990"  # some date that's not today
         response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Post.objects.count(), 2)
@@ -3214,7 +3223,7 @@ class RawIdFieldsTest(TestCase):
         response = self.client.get('/test_admin/admin/admin_views/sketch/add/')
         # Find the link
         m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content)
-        self.assertTrue(m) # Got a match
+        self.assertTrue(m)  # Got a match
         popup_url = m.groups()[0].decode().replace("&amp;", "&")
 
         # Handle relative links
@@ -3224,6 +3233,7 @@ class RawIdFieldsTest(TestCase):
         self.assertContains(response2, "Spain")
         self.assertNotContains(response2, "England")
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class UserAdminTest(TestCase):
     """
@@ -3378,7 +3388,7 @@ class CSSTest(TestCase):
         self.assertContains(response, 'class="form-row field-awesomeness_level"')
         self.assertContains(response, 'class="form-row field-coolness"')
         self.assertContains(response, 'class="form-row field-value"')
-        self.assertContains(response, 'class="form-row"') # The lambda function
+        self.assertContains(response, 'class="form-row"')  # The lambda function
 
         # The tabular inline
         self.assertContains(response, '<td class="field-url">')
@@ -3390,6 +3400,7 @@ try:
 except ImportError:
     docutils = None
 
+
 @unittest.skipUnless(docutils, "no docutils installed.")
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminDocsTest(TestCase):
@@ -3448,7 +3459,7 @@ class ValidXHTMLTests(TestCase):
 
     @override_settings(
         TEMPLATE_CONTEXT_PROCESSORS=filter(
-            lambda t:t!='django.core.context_processors.i18n',
+            lambda t: t != 'django.core.context_processors.i18n',
             global_settings.TEMPLATE_CONTEXT_PROCESSORS),
         USE_I18N=False,
     )
@@ -3585,6 +3596,7 @@ class DateHierarchyTests(TestCase):
             self.assert_non_localized_year(response, 2003)
             self.assert_non_localized_year(response, 2005)
 
+
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class AdminCustomSaveRelatedTests(TestCase):
     """