Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 d84749a
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 531e771
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 29d1abb
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 8a527dd
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
  • Loading branch information...
commit 70a0de37d132e5f1514fb939875f69649f103124 1 parent 7c11b1a
Russell Keith-Magee authored September 26, 2012

Showing 57 changed files with 1,412 additions and 343 deletions. Show diff stats Hide diff stats

  1. 2  django/conf/global_settings.py
  2. 15  django/contrib/admin/forms.py
  3. 6  django/contrib/admin/models.py
  4. 34  django/contrib/admin/sites.py
  5. 2  django/contrib/admin/templates/admin/base.html
  6. 2  django/contrib/admin/templates/admin/login.html
  7. 1  django/contrib/admin/views/decorators.py
  8. 23  django/contrib/auth/__init__.py
  9. 24  django/contrib/auth/backends.py
  10. 14  django/contrib/auth/fixtures/custom_user.json
  11. 15  django/contrib/auth/forms.py
  12. 32  django/contrib/auth/management/__init__.py
  13. 16  django/contrib/auth/management/commands/changepassword.py
  14. 122  django/contrib/auth/management/commands/createsuperuser.py
  15. 173  django/contrib/auth/models.py
  16. 37  django/contrib/auth/tests/__init__.py
  17. 5  django/contrib/auth/tests/auth_backends.py
  18. 45  django/contrib/auth/tests/basic.py
  19. 3  django/contrib/auth/tests/context_processors.py
  20. 75  django/contrib/auth/tests/custom_user.py
  21. 4  django/contrib/auth/tests/decorators.py
  22. 8  django/contrib/auth/tests/forms.py
  23. 100  django/contrib/auth/tests/management.py
  24. 6  django/contrib/auth/tests/models.py
  25. 4  django/contrib/auth/tests/remote_user.py
  26. 2  django/contrib/auth/tests/signals.py
  27. 2  django/contrib/auth/tests/tokens.py
  28. 9  django/contrib/auth/tests/utils.py
  29. 30  django/contrib/auth/tests/views.py
  30. 1  django/contrib/auth/tokens.py
  31. 19  django/contrib/auth/views.py
  32. 17  django/contrib/comments/models.py
  33. 13  django/core/exceptions.py
  34. 1  django/core/management/commands/sqlall.py
  35. 2  django/core/management/commands/syncdb.py
  36. 1  django/core/management/commands/validate.py
  37. 12  django/core/management/sql.py
  38. 27  django/core/management/validation.py
  39. 33  django/core/validators.py
  40. 10  django/db/backends/__init__.py
  41. 16  django/db/backends/creation.py
  42. 20  django/db/models/base.py
  43. 35  django/db/models/fields/related.py
  44. 1  django/db/models/loading.py
  45. 9  django/db/models/manager.py
  46. 21  django/db/models/options.py
  47. 3  django/test/__init__.py
  48. 2  django/test/testcases.py
  49. 7  django/test/utils.py
  50. 3  docs/internals/deprecation.txt
  51. 27  docs/ref/settings.txt
  52. 42  docs/releases/1.5.txt
  53. 359  docs/topics/auth.txt
  54. 110  tests/modeltests/invalid_models/invalid_models/models.py
  55. 13  tests/modeltests/invalid_models/tests.py
  56. 38  tests/modeltests/proxy_models/tests.py
  57. 102  tests/regressiontests/admin_views/tests.py
2  django/conf/global_settings.py
@@ -488,6 +488,8 @@
488 488
 # AUTHENTICATION #
489 489
 ##################
490 490
 
  491
+AUTH_USER_MODEL = 'auth.User'
  492
+
491 493
 AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
492 494
 
493 495
 LOGIN_URL = '/accounts/login/'
15  django/contrib/admin/forms.py
@@ -4,12 +4,12 @@
4 4
 
5 5
 from django.contrib.auth import authenticate
6 6
 from django.contrib.auth.forms import AuthenticationForm
7  
-from django.contrib.auth.models import User
8  
-from django.utils.translation import ugettext_lazy, ugettext as _
  7
+from django.utils.translation import ugettext_lazy
9 8
 
10 9
 ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
11 10
         "for a staff account. Note that both fields are case-sensitive.")
12 11
 
  12
+
13 13
 class AdminAuthenticationForm(AuthenticationForm):
14 14
     """
15 15
     A custom authentication form used in the admin app.
@@ -26,17 +26,6 @@ def clean(self):
26 26
         if username and password:
27 27
             self.user_cache = authenticate(username=username, password=password)
28 28
             if self.user_cache is None:
29  
-                if '@' in username:
30  
-                    # Mistakenly entered e-mail address instead of username? Look it up.
31  
-                    try:
32  
-                        user = User.objects.get(email=username)
33  
-                    except (User.DoesNotExist, User.MultipleObjectsReturned):
34  
-                        # Nothing to do here, moving along.
35  
-                        pass
36  
-                    else:
37  
-                        if user.check_password(password):
38  
-                            message = _("Your e-mail address is not your username."
39  
-                                        " Try '%s' instead.") % user.username
40 29
                 raise forms.ValidationError(message)
41 30
             elif not self.user_cache.is_active or not self.user_cache.is_staff:
42 31
                 raise forms.ValidationError(message)
6  django/contrib/admin/models.py
... ...
@@ -1,8 +1,8 @@
1 1
 from __future__ import unicode_literals
2 2
 
3 3
 from django.db import models
  4
+from django.conf import settings
4 5
 from django.contrib.contenttypes.models import ContentType
5  
-from django.contrib.auth.models import User
6 6
 from django.contrib.admin.util import quote
7 7
 from django.utils.translation import ugettext_lazy as _
8 8
 from django.utils.encoding import smart_text
@@ -12,15 +12,17 @@
12 12
 CHANGE = 2
13 13
 DELETION = 3
14 14
 
  15
+
15 16
 class LogEntryManager(models.Manager):
16 17
     def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
17 18
         e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message)
18 19
         e.save()
19 20
 
  21
+
20 22
 @python_2_unicode_compatible
21 23
 class LogEntry(models.Model):
22 24
     action_time = models.DateTimeField(_('action time'), auto_now=True)
23  
-    user = models.ForeignKey(User)
  25
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
24 26
     content_type = models.ForeignKey(ContentType, blank=True, null=True)
25 27
     object_id = models.TextField(_('object id'), blank=True, null=True)
26 28
     object_repr = models.CharField(_('object repr'), max_length=200)
34  django/contrib/admin/sites.py
@@ -9,7 +9,6 @@
9 9
 from django.core.exceptions import ImproperlyConfigured
10 10
 from django.core.urlresolvers import reverse, NoReverseMatch
11 11
 from django.template.response import TemplateResponse
12  
-from django.utils.safestring import mark_safe
13 12
 from django.utils import six
14 13
 from django.utils.text import capfirst
15 14
 from django.utils.translation import ugettext as _
@@ -18,12 +17,15 @@
18 17
 
19 18
 LOGIN_FORM_KEY = 'this_is_the_login_form'
20 19
 
  20
+
21 21
 class AlreadyRegistered(Exception):
22 22
     pass
23 23
 
  24
+
24 25
 class NotRegistered(Exception):
25 26
     pass
26 27
 
  28
+
27 29
 class AdminSite(object):
28 30
     """
29 31
     An AdminSite object encapsulates an instance of the Django admin application, ready
@@ -41,7 +43,7 @@ class AdminSite(object):
41 43
     password_change_done_template = None
42 44
 
43 45
     def __init__(self, name='admin', app_name='admin'):
44  
-        self._registry = {} # model_class class -> admin_class instance
  46
+        self._registry = {}  # model_class class -> admin_class instance
45 47
         self.name = name
46 48
         self.app_name = app_name
47 49
         self._actions = {'delete_selected': actions.delete_selected}
@@ -80,20 +82,23 @@ def register(self, model_or_iterable, admin_class=None, **options):
80 82
             if model in self._registry:
81 83
                 raise AlreadyRegistered('The model %s is already registered' % model.__name__)
82 84
 
83  
-            # If we got **options then dynamically construct a subclass of
84  
-            # admin_class with those **options.
85  
-            if options:
86  
-                # For reasons I don't quite understand, without a __module__
87  
-                # the created class appears to "live" in the wrong place,
88  
-                # which causes issues later on.
89  
-                options['__module__'] = __name__
90  
-                admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
  85
+            # Ignore the registration if the model has been
  86
+            # swapped out.
  87
+            if not model._meta.swapped:
  88
+                # If we got **options then dynamically construct a subclass of
  89
+                # admin_class with those **options.
  90
+                if options:
  91
+                    # For reasons I don't quite understand, without a __module__
  92
+                    # the created class appears to "live" in the wrong place,
  93
+                    # which causes issues later on.
  94
+                    options['__module__'] = __name__
  95
+                    admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
91 96
 
92  
-            # Validate (which might be a no-op)
93  
-            validate(admin_class, model)
  97
+                # Validate (which might be a no-op)
  98
+                validate(admin_class, model)
94 99
 
95  
-            # Instantiate the admin class to save in the registry
96  
-            self._registry[model] = admin_class(model, self)
  100
+                # Instantiate the admin class to save in the registry
  101
+                self._registry[model] = admin_class(model, self)
97 102
 
98 103
     def unregister(self, model_or_iterable):
99 104
         """
@@ -319,6 +324,7 @@ def login(self, request, extra_context=None):
319 324
             REDIRECT_FIELD_NAME: request.get_full_path(),
320 325
         }
321 326
         context.update(extra_context or {})
  327
+
322 328
         defaults = {
323 329
             'extra_context': context,
324 330
             'current_app': self.name,
2  django/contrib/admin/templates/admin/base.html
@@ -26,7 +26,7 @@
26 26
         {% if user.is_active and user.is_staff %}
27 27
         <div id="user-tools">
28 28
             {% trans 'Welcome,' %}
29  
-            <strong>{% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}</strong>.
  29
+            <strong>{% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}</strong>.
30 30
             {% block userlinks %}
31 31
                 {% url 'django-admindocs-docroot' as docsroot %}
32 32
                 {% if docsroot %}
2  django/contrib/admin/templates/admin/login.html
@@ -30,7 +30,7 @@
30 30
 <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
31 31
   <div class="form-row">
32 32
     {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
33  
-    <label for="id_username" class="required">{% trans 'Username:' %}</label> {{ form.username }}
  33
+    <label for="id_username" class="required">{{ form.username.label }}:</label> {{ form.username }}
34 34
   </div>
35 35
   <div class="form-row">
36 36
     {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}
1  django/contrib/admin/views/decorators.py
@@ -4,6 +4,7 @@
4 4
 from django.contrib.auth.views import login
5 5
 from django.contrib.auth import REDIRECT_FIELD_NAME
6 6
 
  7
+
7 8
 def staff_member_required(view_func):
8 9
     """
9 10
     Decorator for views that checks that the user is logged in and is a staff
23  django/contrib/auth/__init__.py
@@ -6,9 +6,10 @@
6 6
 BACKEND_SESSION_KEY = '_auth_user_backend'
7 7
 REDIRECT_FIELD_NAME = 'next'
8 8
 
  9
+
9 10
 def load_backend(path):
10 11
     i = path.rfind('.')
11  
-    module, attr = path[:i], path[i+1:]
  12
+    module, attr = path[:i], path[i + 1:]
12 13
     try:
13 14
         mod = import_module(module)
14 15
     except ImportError as e:
@@ -21,6 +22,7 @@ def load_backend(path):
21 22
         raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr))
22 23
     return cls()
23 24
 
  25
+
24 26
 def get_backends():
25 27
     from django.conf import settings
26 28
     backends = []
@@ -30,6 +32,7 @@ def get_backends():
30 32
         raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?')
31 33
     return backends
32 34
 
  35
+
33 36
 def authenticate(**credentials):
34 37
     """
35 38
     If the given credentials are valid, return a User object.
@@ -46,6 +49,7 @@ def authenticate(**credentials):
46 49
         user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
47 50
         return user
48 51
 
  52
+
49 53
 def login(request, user):
50 54
     """
51 55
     Persist a user id and a backend in the request. This way a user doesn't
@@ -69,6 +73,7 @@ def login(request, user):
69 73
         request.user = user
70 74
     user_logged_in.send(sender=user.__class__, request=request, user=user)
71 75
 
  76
+
72 77
 def logout(request):
73 78
     """
74 79
     Removes the authenticated user's ID from the request and flushes their
@@ -86,6 +91,22 @@ def logout(request):
86 91
         from django.contrib.auth.models import AnonymousUser
87 92
         request.user = AnonymousUser()
88 93
 
  94
+
  95
+def get_user_model():
  96
+    "Return the User model that is active in this project"
  97
+    from django.conf import settings
  98
+    from django.db.models import get_model
  99
+
  100
+    try:
  101
+        app_label, model_name = settings.AUTH_USER_MODEL.split('.')
  102
+    except ValueError:
  103
+        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
  104
+    user_model = get_model(app_label, model_name)
  105
+    if user_model is None:
  106
+        raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL)
  107
+    return user_model
  108
+
  109
+
89 110
 def get_user(request):
90 111
     from django.contrib.auth.models import AnonymousUser
91 112
     try:
24  django/contrib/auth/backends.py
... ...
@@ -1,6 +1,6 @@
1 1
 from __future__ import unicode_literals
2  
-
3  
-from django.contrib.auth.models import User, Permission
  2
+from django.contrib.auth import get_user_model
  3
+from django.contrib.auth.models import Permission
4 4
 
5 5
 
6 6
 class ModelBackend(object):
@@ -12,10 +12,11 @@ class ModelBackend(object):
12 12
     # configurable.
13 13
     def authenticate(self, username=None, password=None):
14 14
         try:
15  
-            user = User.objects.get(username=username)
  15
+            UserModel = get_user_model()
  16
+            user = UserModel.objects.get_by_natural_key(username)
16 17
             if user.check_password(password):
17 18
                 return user
18  
-        except User.DoesNotExist:
  19
+        except UserModel.DoesNotExist:
19 20
             return None
20 21
 
21 22
     def get_group_permissions(self, user_obj, obj=None):
@@ -60,8 +61,9 @@ def has_module_perms(self, user_obj, app_label):
60 61
 
61 62
     def get_user(self, user_id):
62 63
         try:
63  
-            return User.objects.get(pk=user_id)
64  
-        except User.DoesNotExist:
  64
+            UserModel = get_user_model()
  65
+            return UserModel.objects.get(pk=user_id)
  66
+        except UserModel.DoesNotExist:
65 67
             return None
66 68
 
67 69
 
@@ -94,17 +96,21 @@ def authenticate(self, remote_user):
94 96
         user = None
95 97
         username = self.clean_username(remote_user)
96 98
 
  99
+        UserModel = get_user_model()
  100
+
97 101
         # Note that this could be accomplished in one try-except clause, but
98 102
         # instead we use get_or_create when creating unknown users since it has
99 103
         # built-in safeguards for multiple threads.
100 104
         if self.create_unknown_user:
101  
-            user, created = User.objects.get_or_create(username=username)
  105
+            user, created = UserModel.objects.get_or_create(**{
  106
+                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  107
+            })
102 108
             if created:
103 109
                 user = self.configure_user(user)
104 110
         else:
105 111
             try:
106  
-                user = User.objects.get(username=username)
107  
-            except User.DoesNotExist:
  112
+                user = UserModel.objects.get_by_natural_key(username)
  113
+            except UserModel.DoesNotExist:
108 114
                 pass
109 115
         return user
110 116
 
14  django/contrib/auth/fixtures/custom_user.json
... ...
@@ -0,0 +1,14 @@
  1
+[
  2
+    {
  3
+        "pk": "1",
  4
+        "model": "auth.customuser",
  5
+        "fields": {
  6
+            "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
  7
+            "last_login": "2006-12-17 07:03:31",
  8
+            "email": "staffmember@example.com",
  9
+            "is_active": true,
  10
+            "is_admin": false,
  11
+            "date_of_birth": "1976-11-08"
  12
+       }
  13
+    }
  14
+]
15  django/contrib/auth/forms.py
@@ -7,9 +7,10 @@
7 7
 from django.utils.html import format_html, format_html_join
8 8
 from django.utils.http import int_to_base36
9 9
 from django.utils.safestring import mark_safe
  10
+from django.utils.text import capfirst
10 11
 from django.utils.translation import ugettext, ugettext_lazy as _
11 12
 
12  
-from django.contrib.auth import authenticate
  13
+from django.contrib.auth import authenticate, get_user_model
13 14
 from django.contrib.auth.models import User
14 15
 from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
15 16
 from django.contrib.auth.tokens import default_token_generator
@@ -135,7 +136,7 @@ class AuthenticationForm(forms.Form):
135 136
     Base class for authenticating users. Extend this to get a form that accepts
136 137
     username/password logins.
137 138
     """
138  
-    username = forms.CharField(label=_("Username"), max_length=30)
  139
+    username = forms.CharField(max_length=30)
139 140
     password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
140 141
 
141 142
     error_messages = {
@@ -157,6 +158,11 @@ def __init__(self, request=None, *args, **kwargs):
157 158
         self.user_cache = None
158 159
         super(AuthenticationForm, self).__init__(*args, **kwargs)
159 160
 
  161
+        # Set the label for the "username" field.
  162
+        UserModel = get_user_model()
  163
+        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
  164
+        self.fields['username'].label = capfirst(username_field.verbose_name)
  165
+
160 166
     def clean(self):
161 167
         username = self.cleaned_data.get('username')
162 168
         password = self.cleaned_data.get('password')
@@ -198,9 +204,10 @@ def clean_email(self):
198 204
         """
199 205
         Validates that an active user exists with the given email address.
200 206
         """
  207
+        UserModel = get_user_model()
201 208
         email = self.cleaned_data["email"]
202  
-        self.users_cache = User.objects.filter(email__iexact=email,
203  
-                                               is_active=True)
  209
+        self.users_cache = UserModel.objects.filter(email__iexact=email,
  210
+                                                    is_active=True)
204 211
         if not len(self.users_cache):
205 212
             raise forms.ValidationError(self.error_messages['unknown'])
206 213
         if any((user.password == UNUSABLE_PASSWORD)
32  django/contrib/auth/management/__init__.py
@@ -6,9 +6,10 @@
6 6
 import getpass
7 7
 import locale
8 8
 import unicodedata
9  
-from django.contrib.auth import models as auth_app
  9
+
  10
+from django.contrib.auth import models as auth_app, get_user_model
  11
+from django.core import exceptions
10 12
 from django.db.models import get_models, signals
11  
-from django.contrib.auth.models import User
12 13
 from django.utils import six
13 14
 from django.utils.six.moves import input
14 15
 
@@ -64,7 +65,9 @@ def create_permissions(app, created_models, verbosity, **kwargs):
64 65
 def create_superuser(app, created_models, verbosity, db, **kwargs):
65 66
     from django.core.management import call_command
66 67
 
67  
-    if auth_app.User in created_models and kwargs.get('interactive', True):
  68
+    UserModel = get_user_model()
  69
+
  70
+    if UserModel in created_models and kwargs.get('interactive', True):
68 71
         msg = ("\nYou just installed Django's auth system, which means you "
69 72
             "don't have any superusers defined.\nWould you like to create one "
70 73
             "now? (yes/no): ")
@@ -113,28 +116,35 @@ def get_default_username(check_db=True):
113 116
     :returns: The username, or an empty string if no username can be
114 117
         determined.
115 118
     """
116  
-    from django.contrib.auth.management.commands.createsuperuser import (
117  
-        RE_VALID_USERNAME)
  119
+    # If the User model has been swapped out, we can't make any assumptions
  120
+    # about the default user name.
  121
+    if auth_app.User._meta.swapped:
  122
+        return ''
  123
+
118 124
     default_username = get_system_username()
119 125
     try:
120 126
         default_username = unicodedata.normalize('NFKD', default_username)\
121 127
             .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower()
122 128
     except UnicodeDecodeError:
123 129
         return ''
124  
-    if not RE_VALID_USERNAME.match(default_username):
  130
+
  131
+    # Run the username validator
  132
+    try:
  133
+        auth_app.User._meta.get_field('username').run_validators(default_username)
  134
+    except exceptions.ValidationError:
125 135
         return ''
  136
+
126 137
     # Don't return the default username if it is already taken.
127 138
     if check_db and default_username:
128 139
         try:
129  
-            User.objects.get(username=default_username)
130  
-        except User.DoesNotExist:
  140
+            auth_app.User.objects.get(username=default_username)
  141
+        except auth_app.User.DoesNotExist:
131 142
             pass
132 143
         else:
133 144
             return ''
134 145
     return default_username
135 146
 
136  
-
137 147
 signals.post_syncdb.connect(create_permissions,
138  
-    dispatch_uid = "django.contrib.auth.management.create_permissions")
  148
+    dispatch_uid="django.contrib.auth.management.create_permissions")
139 149
 signals.post_syncdb.connect(create_superuser,
140  
-    sender=auth_app, dispatch_uid = "django.contrib.auth.management.create_superuser")
  150
+    sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser")
16  django/contrib/auth/management/commands/changepassword.py
... ...
@@ -1,8 +1,8 @@
1 1
 import getpass
2 2
 from optparse import make_option
3 3
 
  4
+from django.contrib.auth import get_user_model
4 5
 from django.core.management.base import BaseCommand, CommandError
5  
-from django.contrib.auth.models import User
6 6
 from django.db import DEFAULT_DB_ALIAS
7 7
 
8 8
 
@@ -30,12 +30,16 @@ def handle(self, *args, **options):
30 30
         else:
31 31
             username = getpass.getuser()
32 32
 
  33
+        UserModel = get_user_model()
  34
+
33 35
         try:
34  
-            u = User.objects.using(options.get('database')).get(username=username)
35  
-        except User.DoesNotExist:
  36
+            u = UserModel.objects.using(options.get('database')).get(**{
  37
+                    getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  38
+                })
  39
+        except UserModel.DoesNotExist:
36 40
             raise CommandError("user '%s' does not exist" % username)
37 41
 
38  
-        self.stdout.write("Changing password for user '%s'\n" % u.username)
  42
+        self.stdout.write("Changing password for user '%s'\n" % u)
39 43
 
40 44
         MAX_TRIES = 3
41 45
         count = 0
@@ -48,9 +52,9 @@ def handle(self, *args, **options):
48 52
                 count = count + 1
49 53
 
50 54
         if count == MAX_TRIES:
51  
-            raise CommandError("Aborting password change for user '%s' after %s attempts" % (username, count))
  55
+            raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count))
52 56
 
53 57
         u.set_password(p1)
54 58
         u.save()
55 59
 
56  
-        return "Password changed successfully for user '%s'" % u.username
  60
+        return "Password changed successfully for user '%s'" % u
122  django/contrib/auth/management/commands/createsuperuser.py
@@ -3,109 +3,114 @@
3 3
 """
4 4
 
5 5
 import getpass
6  
-import re
7 6
 import sys
8 7
 from optparse import make_option
9 8
 
10  
-from django.contrib.auth.models import User
  9
+from django.contrib.auth import get_user_model
11 10
 from django.contrib.auth.management import get_default_username
12 11
 from django.core import exceptions
13 12
 from django.core.management.base import BaseCommand, CommandError
14 13
 from django.db import DEFAULT_DB_ALIAS
15 14
 from django.utils.six.moves import input
16  
-from django.utils.translation import ugettext as _
17  
-
18  
-RE_VALID_USERNAME = re.compile('[\w.@+-]+$')
19  
-
20  
-EMAIL_RE = re.compile(
21  
-    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
22  
-    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string
23  
-    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
24  
-
25  
-
26  
-def is_valid_email(value):
27  
-    if not EMAIL_RE.search(value):
28  
-        raise exceptions.ValidationError(_('Enter a valid e-mail address.'))
  15
+from django.utils.text import capfirst
29 16
 
30 17
 
31 18
 class Command(BaseCommand):
32 19
     option_list = BaseCommand.option_list + (
33 20
         make_option('--username', dest='username', default=None,
34 21
             help='Specifies the username for the superuser.'),
35  
-        make_option('--email', dest='email', default=None,
36  
-            help='Specifies the email address for the superuser.'),
37 22
         make_option('--noinput', action='store_false', dest='interactive', default=True,
38 23
             help=('Tells Django to NOT prompt the user for input of any kind. '
39  
-                  'You must use --username and --email with --noinput, and '
40  
-                  'superusers created with --noinput will not be able to log '
41  
-                  'in until they\'re given a valid password.')),
  24
+                  'You must use --username with --noinput, along with an option for '
  25
+                  'any other required field. Superusers created with --noinput will '
  26
+                  ' not be able to log in until they\'re given a valid password.')),
42 27
         make_option('--database', action='store', dest='database',
43 28
             default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'),
  29
+    ) + tuple(
  30
+        make_option('--%s' % field, dest=field, default=None,
  31
+            help='Specifies the %s for the superuser.' % field)
  32
+        for field in get_user_model().REQUIRED_FIELDS
44 33
     )
  34
+
45 35
     help = 'Used to create a superuser.'
46 36
 
47 37
     def handle(self, *args, **options):
48 38
         username = options.get('username', None)
49  
-        email = options.get('email', None)
50 39
         interactive = options.get('interactive')
51 40
         verbosity = int(options.get('verbosity', 1))
52 41
         database = options.get('database')
53 42
 
54  
-        # Do quick and dirty validation if --noinput
55  
-        if not interactive:
56  
-            if not username or not email:
57  
-                raise CommandError("You must use --username and --email with --noinput.")
58  
-            if not RE_VALID_USERNAME.match(username):
59  
-                raise CommandError("Invalid username. Use only letters, digits, and underscores")
60  
-            try:
61  
-                is_valid_email(email)
62  
-            except exceptions.ValidationError:
63  
-                raise CommandError("Invalid email address.")
  43
+        UserModel = get_user_model()
  44
+
  45
+        username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
  46
+        other_fields = UserModel.REQUIRED_FIELDS
64 47
 
65 48
         # If not provided, create the user with an unusable password
66 49
         password = None
  50
+        other_data = {}
67 51
 
68  
-        # Prompt for username/email/password. Enclose this whole thing in a
69  
-        # try/except to trap for a keyboard interrupt and exit gracefully.
70  
-        if interactive:
  52
+        # Do quick and dirty validation if --noinput
  53
+        if not interactive:
  54
+            try:
  55
+                if not username:
  56
+                    raise CommandError("You must use --username with --noinput.")
  57
+                username = username_field.clean(username, None)
  58
+
  59
+                for field_name in other_fields:
  60
+                    if options.get(field_name):
  61
+                        field = UserModel._meta.get_field(field_name)
  62
+                        other_data[field_name] = field.clean(options[field_name], None)
  63
+                    else:
  64
+                        raise CommandError("You must use --%s with --noinput." % field_name)
  65
+            except exceptions.ValidationError as e:
  66
+                raise CommandError('; '.join(e.messages))
  67
+
  68
+        else:
  69
+            # Prompt for username/password, and any other required fields.
  70
+            # Enclose this whole thing in a try/except to trap for a
  71
+            # keyboard interrupt and exit gracefully.
71 72
             default_username = get_default_username()
72 73
             try:
73 74
 
74 75
                 # Get a username
75  
-                while 1:
  76
+                while username is None:
  77
+                    username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username'))
76 78
                     if not username:
77  
-                        input_msg = 'Username'
  79
+                        input_msg = capfirst(username_field.verbose_name)
78 80
                         if default_username:
79 81
                             input_msg += ' (leave blank to use %r)' % default_username
80  
-                        username = input(input_msg + ': ')
81  
-                    if default_username and username == '':
  82
+                        raw_value = input(input_msg + ': ')
  83
+                    if default_username and raw_value == '':
82 84
                         username = default_username
83  
-                    if not RE_VALID_USERNAME.match(username):
84  
-                        self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.")
  85
+                    try:
  86
+                        username = username_field.clean(raw_value, None)
  87
+                    except exceptions.ValidationError as e:
  88
+                        self.stderr.write("Error: %s" % '; '.join(e.messages))
85 89
                         username = None
86 90
                         continue
87 91
                     try:
88  
-                        User.objects.using(database).get(username=username)
89  
-                    except User.DoesNotExist:
90  
-                        break
  92
+                        UserModel.objects.using(database).get(**{
  93
+                                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  94
+                            })
  95
+                    except UserModel.DoesNotExist:
  96
+                        pass
91 97
                     else:
92 98
                         self.stderr.write("Error: That username is already taken.")
93 99
                         username = None
94 100
 
95  
-                # Get an email
96  
-                while 1:
97  
-                    if not email:
98  
-                        email = input('E-mail address: ')
99  
-                    try:
100  
-                        is_valid_email(email)
101  
-                    except exceptions.ValidationError:
102  
-                        self.stderr.write("Error: That e-mail address is invalid.")
103  
-                        email = None
104  
-                    else:
105  
-                        break
  101
+                for field_name in other_fields:
  102
+                    field = UserModel._meta.get_field(field_name)
  103
+                    other_data[field_name] = options.get(field_name)
  104
+                    while other_data[field_name] is None:
  105
+                        raw_value = input(capfirst(field.verbose_name + ': '))
  106
+                        try:
  107
+                            other_data[field_name] = field.clean(raw_value, None)
  108
+                        except exceptions.ValidationError as e:
  109
+                            self.stderr.write("Error: %s" % '; '.join(e.messages))
  110
+                            other_data[field_name] = None
106 111
 
107 112
                 # Get a password
108  
-                while 1:
  113
+                while password is None:
109 114
                     if not password:
110 115
                         password = getpass.getpass()
111 116
                         password2 = getpass.getpass('Password (again): ')
@@ -117,12 +122,11 @@ def handle(self, *args, **options):
117 122
                         self.stderr.write("Error: Blank passwords aren't allowed.")
118 123
                         password = None
119 124
                         continue
120  
-                    break
  125
+
121 126
             except KeyboardInterrupt:
122 127
                 self.stderr.write("\nOperation cancelled.")
123 128
                 sys.exit(1)
124 129
 
125  
-        User.objects.db_manager(database).create_superuser(username, email, password)
  130
+        UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data)
126 131
         if verbosity >= 1:
127  
-          self.stdout.write("Superuser created successfully.")
128  
-
  132
+            self.stdout.write("Superuser created successfully.")
173  django/contrib/auth/models.py
... ...
@@ -1,7 +1,10 @@
1 1
 from __future__ import unicode_literals
  2
+import re
  3
+import warnings
2 4
 
3 5
 from django.core.exceptions import ImproperlyConfigured
4 6
 from django.core.mail import send_mail
  7
+from django.core import validators
5 8
 from django.db import models
6 9
 from django.db.models.manager import EmptyManager
7 10
 from django.utils.crypto import get_random_string
@@ -96,6 +99,7 @@ class GroupManager(models.Manager):
96 99
     def get_by_natural_key(self, name):
97 100
         return self.get(name=name)
98 101
 
  102
+
99 103
 @python_2_unicode_compatible
100 104
 class Group(models.Model):
101 105
     """
@@ -131,7 +135,7 @@ def natural_key(self):
131 135
         return (self.name,)
132 136
 
133 137
 
134  
-class UserManager(models.Manager):
  138
+class BaseUserManager(models.Manager):
135 139
 
136 140
     @classmethod
137 141
     def normalize_email(cls, email):
@@ -148,7 +152,25 @@ def normalize_email(cls, email):
148 152
             email = '@'.join([email_name, domain_part.lower()])
149 153
         return email
150 154
 
151  
-    def create_user(self, username, email=None, password=None):
  155
+    def make_random_password(self, length=10,
  156
+                             allowed_chars='abcdefghjkmnpqrstuvwxyz'
  157
+                                           'ABCDEFGHJKLMNPQRSTUVWXYZ'
  158
+                                           '23456789'):
  159
+        """
  160
+        Generates a random password with the given length and given
  161
+        allowed_chars. Note that the default value of allowed_chars does not
  162
+        have "I" or "O" or letters and digits that look similar -- just to
  163
+        avoid confusion.
  164
+        """
  165
+        return get_random_string(length, allowed_chars)
  166
+
  167
+    def get_by_natural_key(self, username):
  168
+        return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username})
  169
+
  170
+
  171
+class UserManager(BaseUserManager):
  172
+
  173
+    def create_user(self, username, email=None, password=None, **extra_fields):
152 174
         """
153 175
         Creates and saves a User with the given username, email and password.
154 176
         """
@@ -158,35 +180,20 @@ def create_user(self, username, email=None, password=None):
158 180
         email = UserManager.normalize_email(email)
159 181
         user = self.model(username=username, email=email,
160 182
                           is_staff=False, is_active=True, is_superuser=False,
161  
-                          last_login=now, date_joined=now)
  183
+                          last_login=now, date_joined=now, **extra_fields)
162 184
 
163 185
         user.set_password(password)
164 186
         user.save(using=self._db)
165 187
         return user
166 188
 
167  
-    def create_superuser(self, username, email, password):
168  
-        u = self.create_user(username, email, password)
  189
+    def create_superuser(self, username, email, password, **extra_fields):
  190
+        u = self.create_user(username, email, password, **extra_fields)
169 191
         u.is_staff = True
170 192
         u.is_active = True
171 193
         u.is_superuser = True
172 194
         u.save(using=self._db)
173 195
         return u
174 196
 
175  
-    def make_random_password(self, length=10,
176  
-                             allowed_chars='abcdefghjkmnpqrstuvwxyz'
177  
-                                           'ABCDEFGHJKLMNPQRSTUVWXYZ'
178  
-                                           '23456789'):
179  
-        """
180  
-        Generates a random password with the given length and given
181  
-        allowed_chars. Note that the default value of allowed_chars does not
182  
-        have "I" or "O" or letters and digits that look similar -- just to
183  
-        avoid confusion.
184  
-        """
185  
-        return get_random_string(length, allowed_chars)
186  
-
187  
-    def get_by_natural_key(self, username):
188  
-        return self.get(username=username)
189  
-
190 197
 
191 198
 # A few helper functions for common logic between User and AnonymousUser.
192 199
 def _user_get_all_permissions(user, obj):
@@ -201,8 +208,6 @@ def _user_get_all_permissions(user, obj):
201 208
 
202 209
 
203 210
 def _user_has_perm(user, perm, obj):
204  
-    anon = user.is_anonymous()
205  
-    active = user.is_active
206 211
     for backend in auth.get_backends():
207 212
         if hasattr(backend, "has_perm"):
208 213
             if obj is not None:
@@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj):
215 220
 
216 221
 
217 222
 def _user_has_module_perms(user, app_label):
218  
-    anon = user.is_anonymous()
219  
-    active = user.is_active
220 223
     for backend in auth.get_backends():
221 224
         if hasattr(backend, "has_module_perms"):
222 225
             if backend.has_module_perms(user, app_label):
@@ -224,21 +227,73 @@ def _user_has_module_perms(user, app_label):
224 227
     return False
225 228
 
226 229
 
  230
+class AbstractBaseUser(models.Model):
  231
+    password = models.CharField(_('password'), max_length=128)
  232
+    last_login = models.DateTimeField(_('last login'), default=timezone.now)
  233
+
  234
+    REQUIRED_FIELDS = []
  235
+
  236
+    class Meta:
  237
+        abstract = True
  238
+
  239
+    def is_anonymous(self):
  240
+        """
  241
+        Always returns False. This is a way of comparing User objects to
  242
+        anonymous users.
  243
+        """
  244
+        return False
  245
+
  246
+    def is_authenticated(self):
  247
+        """
  248
+        Always return True. This is a way to tell if the user has been
  249
+        authenticated in templates.
  250
+        """
  251
+        return True
  252
+
  253
+    def set_password(self, raw_password):
  254
+        self.password = make_password(raw_password)
  255
+
  256
+    def check_password(self, raw_password):
  257
+        """
  258
+        Returns a boolean of whether the raw_password was correct. Handles
  259
+        hashing formats behind the scenes.
  260
+        """
  261
+        def setter(raw_password):
  262
+            self.set_password(raw_password)
  263
+            self.save(update_fields=["password"])
  264
+        return check_password(raw_password, self.password, setter)
  265
+
  266
+    def set_unusable_password(self):
  267
+        # Sets a value that will never be a valid hash
  268
+        self.password = make_password(None)
  269
+
  270
+    def has_usable_password(self):
  271
+        return is_password_usable(self.password)
  272
+
  273
+    def get_full_name(self):
  274
+        raise NotImplementedError()
  275
+
  276
+    def get_short_name(self):
  277
+        raise NotImplementedError()
  278
+
  279
+
227 280
 @python_2_unicode_compatible
228  
-class User(models.Model):
  281
+class AbstractUser(AbstractBaseUser):
229 282
     """
230  
-    Users within the Django authentication system are represented by this
231  
-    model.
  283
+    An abstract base class implementing a fully featured User model with
  284
+    admin-compliant permissions.
232 285
 
233  
-    Username and password are required. Other fields are optional.
  286
+    Username, password and email are required. Other fields are optional.
234 287
     """
235 288
     username = models.CharField(_('username'), max_length=30, unique=True,
236 289
         help_text=_('Required. 30 characters or fewer. Letters, numbers and '
237  
-                    '@/./+/-/_ characters'))
  290
+                    '@/./+/-/_ characters'),
  291
+        validators=[
  292
+            validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
  293
+        ])
238 294
     first_name = models.CharField(_('first name'), max_length=30, blank=True)
239 295
     last_name = models.CharField(_('last name'), max_length=30, blank=True)
240  
-    email = models.EmailField(_('e-mail address'), blank=True)
241  
-    password = models.CharField(_('password'), max_length=128)
  296
+    email = models.EmailField(_('email address'), blank=True)
242 297
     is_staff = models.BooleanField(_('staff status'), default=False,
243 298
         help_text=_('Designates whether the user can log into this admin '
244 299
                     'site.'))
@@ -248,7 +303,6 @@ class User(models.Model):
248 303
     is_superuser = models.BooleanField(_('superuser status'), default=False,
249 304
         help_text=_('Designates that this user has all permissions without '
250 305
                     'explicitly assigning them.'))
251  
-    last_login = models.DateTimeField(_('last login'), default=timezone.now)
252 306
     date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
253 307
     groups = models.ManyToManyField(Group, verbose_name=_('groups'),
254 308
         blank=True, help_text=_('The groups this user belongs to. A user will '
@@ -257,11 +311,15 @@ class User(models.Model):
257 311
     user_permissions = models.ManyToManyField(Permission,
258 312
         verbose_name=_('user permissions'), blank=True,
259 313
         help_text='Specific permissions for this user.')
  314
+
260 315
     objects = UserManager()
261 316
 
  317
+    REQUIRED_FIELDS = ['email']
  318
+
262 319
     class Meta:
263 320
         verbose_name = _('user')
264 321
         verbose_name_plural = _('users')
  322
+        abstract = True
265 323
 
266 324
     def __str__(self):
267 325
         return self.username
@@ -272,20 +330,6 @@ def natural_key(self):
272 330
     def get_absolute_url(self):
273 331
         return "/users/%s/" % urlquote(self.username)
274 332
 
275  
-    def is_anonymous(self):
276  
-        """
277  
-        Always returns False. This is a way of comparing User objects to
278  
-        anonymous users.
279  
-        """
280  
-        return False
281  
-
282  
-    def is_authenticated(self):
283  
-        """
284  
-        Always return True. This is a way to tell if the user has been
285  
-        authenticated in templates.
286  
-        """
287  
-        return True
288  
-
289 333
     def get_full_name(self):
290 334
         """
291 335
         Returns the first_name plus the last_name, with a space in between.
@@ -293,25 +337,9 @@ def get_full_name(self):
293 337
         full_name = '%s %s' % (self.first_name, self.last_name)
294 338
         return full_name.strip()
295 339
 
296  
-    def set_password(self, raw_password):
297  
-        self.password = make_password(raw_password)
298  
-
299  
-    def check_password(self, raw_password):
300  
-        """
301  
-        Returns a boolean of whether the raw_password was correct. Handles
302  
-        hashing formats behind the scenes.
303  
-        """
304  
-        def setter(raw_password):
305  
-            self.set_password(raw_password)
306  
-            self.save(update_fields=["password"])
307  
-        return check_password(raw_password, self.password, setter)
308  
-
309  
-    def set_unusable_password(self):
310  
-        # Sets a value that will never be a valid hash
311  
-        self.password = make_password(None)
312  
-
313  
-    def has_usable_password(self):
314  
-        return is_password_usable(self.password)
  340
+    def get_short_name(self):
  341
+        "Returns the short name for the user."
  342
+        return self.first_name
315 343
 
316 344
     def get_group_permissions(self, obj=None):
317 345
         """
@@ -381,6 +409,8 @@ def get_profile(self):
381 409
         Returns site-specific profile for this user. Raises
382 410
         SiteProfileNotAvailable if this site does not allow profiles.
383 411
         """
  412
+        warnings.warn("The use of AUTH_PROFILE_MODULE to define user profiles has been deprecated.",
  413
+            PendingDeprecationWarning)
384 414
         if not hasattr(self, '_profile_cache'):
385 415
             from django.conf import settings
386 416
             if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
@@ -407,6 +437,17 @@ def get_profile(self):
407 437
         return self._profile_cache
408 438
 
409 439
 
  440
+class User(AbstractUser):
  441
+    """
  442
+    Users within the Django authentication system are represented by this
  443
+    model.
  444
+
  445
+    Username, password and email are required. Other fields are optional.
  446
+    """
  447
+    class Meta:
  448
+        swappable = 'AUTH_USER_MODEL'
  449
+
  450
+
410 451
 @python_2_unicode_compatible
411 452
 class AnonymousUser(object):
412 453
     id = None
@@ -431,7 +472,7 @@ def __ne__(self, other):
431 472
         return not self.__eq__(other)
432 473
 
433 474
     def __hash__(self):
434  
-        return 1 # instances always return the same hash value
  475
+        return 1  # instances always return the same hash value
435 476
 
436 477
     def save(self):
437 478
         raise NotImplementedError
37  django/contrib/auth/tests/__init__.py
... ...
@@ -1,26 +1,15 @@
1  
-from django.contrib.auth.tests.auth_backends import (BackendTest,
2  
-    RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
3  
-    InActiveUserBackendTest)
4  
-from django.contrib.auth.tests.basic import BasicTestCase
5  
-from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
6  
-from django.contrib.auth.tests.decorators import LoginRequiredTestCase
7  
-from django.contrib.auth.tests.forms import (UserCreationFormTest,
8  
-    AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest,
9  
-    UserChangeFormTest, PasswordResetFormTest)
10  
-from django.contrib.auth.tests.remote_user import (RemoteUserTest,
11  
-    RemoteUserNoCreateTest, RemoteUserCustomTest)
12  
-from django.contrib.auth.tests.management import (
13  
-    GetDefaultUsernameTestCase,
14  
-    ChangepasswordManagementCommandTestCase,
15  
-)
16  
-from django.contrib.auth.tests.models import (ProfileTestCase, NaturalKeysTestCase,
17  
-    LoadDataWithoutNaturalKeysTestCase, LoadDataWithNaturalKeysTestCase,
18  
-    UserManagerTestCase)
19  
-from django.contrib.auth.tests.hashers import TestUtilsHashPass
20  
-from django.contrib.auth.tests.signals import SignalTestCase
21  
-from django.contrib.auth.tests.tokens import TokenGeneratorTest
22  
-from django.contrib.auth.tests.views import (AuthViewNamedURLTests,
23  
-    PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest,
24  
-    LoginURLSettings)
  1
+from django.contrib.auth.tests.custom_user import *
  2
+from django.contrib.auth.tests.auth_backends import *
  3
+from django.contrib.auth.tests.basic import *
  4
+from django.contrib.auth.tests.context_processors import *
  5
+from django.contrib.auth.tests.decorators import *
  6
+from django.contrib.auth.tests.forms import *
  7
+from django.contrib.auth.tests.remote_user import *
  8
+from django.contrib.auth.tests.management import *
  9
+from django.contrib.auth.tests.models import *
  10
+from django.contrib.auth.tests.hashers import *
  11
+from django.contrib.auth.tests.signals import *
  12
+from django.contrib.auth.tests.tokens import *
  13
+from django.contrib.auth.tests.views import *
25 14
 
26 15
 # The password for the fixture data users is 'password'
5  django/contrib/auth/tests/auth_backends.py
@@ -2,12 +2,14 @@
2 2
 
3 3
 from django.conf import settings
4 4
 from django.contrib.auth.models import User, Group, Permission, AnonymousUser
  5
+from django.contrib.auth.tests.utils import skipIfCustomUser
5 6
 from django.contrib.contenttypes.models import ContentType
6 7
 from django.core.exceptions import ImproperlyConfigured
7 8
 from django.test import TestCase
8 9
 from django.test.utils import override_settings
9 10
 
10 11
 
  12
+@skipIfCustomUser
11 13
 class BackendTest(TestCase):
12 14
 
13 15
     backend = 'django.contrib.auth.backends.ModelBackend'
@@ -151,6 +153,7 @@ def get_group_permissions(self, user, obj=None):
151 153
             return ['none']
152 154
 
153 155
 
  156
+@skipIfCustomUser
154 157
 class RowlevelBackendTest(TestCase):
155 158
     """
156 159
     Tests for auth backend that supports object level permissions
@@ -223,6 +226,7 @@ def test_get_all_permissions(self):
223 226
         self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon']))
224 227
 
225 228
 
  229
+@skipIfCustomUser
226 230
 @override_settings(AUTHENTICATION_BACKENDS=[])
227 231
 class NoBackendsTest(TestCase):
228 232
     """
@@ -235,6 +239,7 @@ def test_raises_exception(self):
235 239
         self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),))
236 240
 
237 241
 
  242
+@skipIfCustomUser
238 243
 class InActiveUserBackendTest(TestCase):
239 244
     ""