Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Custom user models (#3011) #370

Closed
wants to merge 41 commits into from

13 participants

Jacob Kaplan-Moss Dan Loewenherz Florian Apolloner Russell Keith-Magee Alex Gaynor ironfroggy Alexey Boriskin Ramiro Morales Preston Holmes Vinicius Ruan Cainelli Rafal Stozek Thomas Sutton Anssi Kääriäinen
Jacob Kaplan-Moss
Owner

No description provided.

Rafal Stozek

Those tests may fail when using custom user model (eg. when create_user() does not accept username param).

Owner

Skip is the best solution here. But what about other tests in django or third party apps? Maybe there should be a hook for creating test users - create_test_user() on model manager. It could take arguments like username, email, is_staff and ignore them if they are not used.

But then we need solution for authenticating users in tests.

and others added some commits
Jacob Kaplan-Moss jacobian commented on the diff
django/contrib/admin/forms.py
@@ -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
3
Jacob Kaplan-Moss Owner

Did this "@" in username check get moved elsewhere, or just removed? It looks like the latter. That's fine by me, but it is technically a backwards-incompatible change, so it should be mentioned in the release notes.

Russell Keith-Magee Owner

This check was removed because there's no guarantee that an email field exists anymore.

It might be possible to resurrect it by abstracting the underlying idea onto an optional method on the User class, but frankly, my inclination is to just kill the feature.

Jacob Kaplan-Moss Owner

I'm fine with that - kill away. Let's just add a release note about it since it's theoretically backwards-incompatible (albeit in an incredibly minor way.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Dan Loewenherz

First of all, this looks amazing. Fantastic job, I can't wait to use this in production! I've been skirting around the issue by implementing a custom auth backend with my own user model for months now (which worked), but the admin access was always confusing for clients.

I'm looking through here, and I still see first_name and last_name as required fields for the abstract user model. Any reason these should still be required?

django/contrib/auth/backends.py
@@ -12,10 +12,13 @@ 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(**{
  17
+                getattr(UserModel, 'USERNAME_FIELD', 'username'): username
  18
+            })
4
Jacob Kaplan-Moss Owner

I wonder if this might be better done with a manager method - UserModel.objects.get_by_username('username') - to avoid the need for the USERNAME_FIELD indirection. That feels a bit cleaner to me, and seems to better encapsulate the query in the manager where it belongs.

Jacob Kaplan-Moss Owner

Yeah, looking further, it appears this idiom is used a few more times, which to me triggers Fowler's "Third Time Refactor" rule. I think pushing this logic down into a manager method is going to lead to cleaner code (and easier-to-write code for end-users as well).

Russell Keith-Magee Owner

Fair call - I'll factor this out into a manager method.

Russell Keith-Magee Owner

Turns out the method already exists: get_by_natural_key(). I've modified the code to use this method as appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/contrib/auth/management/commands/createsuperuser.py
((45 lines not shown))
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 getattr(get_user_model(), 'REQUIRED_FIELDS', ['email'])
2
Jacob Kaplan-Moss Owner

I can't say I'm thrilled with the way these required fields are handled in this command - something here isn't passing the smell test. I understand why it's needed -- otherwise the createsuperuser command fails hard on custom user models -- and I can't put my finger on exactly what feels wrong. Nor can I suggest a better approach, so in all likelihood this should go in as-is. However, I'm flagging my discomfort so that we can think about it just a little bit and see if there's not something a little less... smelly.

Russell Keith-Magee Owner

Agreed with the code smell, but not sure I see a better option (other than not allowing command-line specification of non-username arguments). Suggestions welcome, but I can probably live with the code smell.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alex Gaynor alex commented on the diff
django/contrib/auth/management/__init__.py
((11 lines not shown))
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)
4
Alex Gaynor Owner
alex added a note

Use get_field_by_name('username')[0], get_field is O(n) whereas get_field_by_name is O(1). No joke.

Florian Apolloner Owner

Rofl, shouldn't we just change get_field to get_field_by_name('bla')[0] then? Using ugly API to be more performent smells bad if you ask me.

Russell Keith-Magee Owner

Hell yeah - fix the cause, not the symptom.

Russell Keith-Magee Owner

Turns out it's not quite as simple as I thought...

Firstly - Yes, it's O(n), but, N is very small, and the way get_field_by_name() is O(1) is by pre-caching all possible lookups, so there's a first-call cache warming expense. In this usage, we're on a management command, and get_field will be invoked exactly once, so we're going to be in territory where the expense of cache warming to get the O(1) lookup will exceed the O(N) cost we're trying to avoid.

Secondly, it's not as simple as replacing the internals of get_field() with get_field_by_name(), because the cache priming in get_field_by_name() assumes all the models have been loaded (because it includes all reverse FK and M2M relations). get_field() only depends on self. This should get a little cleaner once app loading lands, but as it currently stands, it's difficult to guarantee that get_field_by_name() is actually callable.

So - I'm going to call this a no-fix; or, at least, a "not my bailiwick" fix. Meta certainly could be cleaned up, but that's a much bigger fish that needs frying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/test/testcases.py
@@ -906,6 +908,13 @@ def skipUnlessDBFeature(feature):
906 908
                          "Database doesn't support feature %s" % feature)
907 909
 
908 910
 
  911
+def skipIfCustomUser(test_func):
  912
+    """
  913
+    Skip a test if a custom user model is in use.
  914
+    """
  915
+    return skipIf(settings.AUTH_USER_MODEL != 'auth.User', 'Custom user model in use')(test_func)
  916
+
  917
+
3
Jacob Kaplan-Moss Owner

Shouldn't this decorator live in django.contrib.auth somewhere instead of here?

Alex Gaynor Owner
alex added a note
Russell Keith-Magee Owner

Fair call. I'll move it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
docs/topics/auth.txt
@@ -57,6 +57,10 @@ API reference
57 57
 Fields
58 58
 ~~~~~~
59 59
 
  60
+TODO document which attributes/methods come from AbstractBaseUser
  61
+TODO tone down references to get_profile - it's not the best way of doing things
  62
+any more.
  63
+
2
Jacob Kaplan-Moss Owner

These probably have to be TO-DONE before the patch lands. At the very least, make 'em into comments so they don't show up in the rendered output and make us look unprofessional :)

Russell Keith-Magee Owner

I'm pretty sure these TODOs are TO-DONE; but I'll check to be sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jacob Kaplan-Moss jacobian commented on the diff
docs/topics/auth.txt
((15 lines not shown))
  1770
+
  1771
+Django allows you to override the default User model by providing a value for
  1772
+the :setting:`AUTH_USER_MODEL` setting that references a custom model::
  1773
+
  1774
+     AUTH_USER_MODEL = 'myapp.MyUser'
  1775
+
  1776
+This dotted pair describes the name of the Django app, and the name of the Django
  1777
+model that you wish to use as your User model.
  1778
+
  1779
+.. admonition:: Warning
  1780
+
  1781
+   Changing :setting:`AUTH_USER_MODEL` has a big effect on your database
  1782
+   structure. It changes the tables that are available, and it will affect the
  1783
+   construction of foreign keys and many-to-many relationships. If you intend
  1784
+   to set :setting:`AUTH_USER_MODEL`, you should set it before running
  1785
+   ``manage.py syncdb`` for the first time.
1
Jacob Kaplan-Moss Owner

Perhaps here's a good point to mention using South if you do need to change it post-facto.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jacob Kaplan-Moss jacobian commented on the diff
docs/topics/auth.txt
((23 lines not shown))
  1778
+
  1779
+.. admonition:: Warning
  1780
+
  1781
+   Changing :setting:`AUTH_USER_MODEL` has a big effect on your database
  1782
+   structure. It changes the tables that are available, and it will affect the
  1783
+   construction of foreign keys and many-to-many relationships. If you intend
  1784
+   to set :setting:`AUTH_USER_MODEL`, you should set it before running
  1785
+   ``manage.py syncdb`` for the first time.
  1786
+
  1787
+Referencing the User model
  1788
+--------------------------
  1789
+
  1790
+If you reference :class:`~django.contrib.auth.models.User` directly (for
  1791
+example, by referring to it in a foreign key), your code will not work in
  1792
+projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a
  1793
+different User model.
1
Jacob Kaplan-Moss Owner

Maybe mention that this is appropriate for reusable/pluggable models, but not really needed in projects where you know what your user model is and where it's not going to change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
docs/topics/auth.txt
((70 lines not shown))
  1825
+easiest way to construct a compliant custom User model is to inherit from
  1826
+:class:`~django.contrib.auth.models.AbstractBaseUser` and provide some key
  1827
+definitions:
  1828
+
  1829
+.. attribute:: User.USERNAME_FIELD
  1830
+
  1831
+    A string describing the name of the field on the User model that is
  1832
+    used as the unique identifier. This will usually be a username of
  1833
+    some kind, but it can also be an email address, or any other unique
  1834
+    identifier.
  1835
+
  1836
+.. attribute:: User.REQUIRED_FIELDS
  1837
+
  1838
+    A list of the field names that *must* be provided when creating
  1839
+    a user.
  1840
+
2
Jacob Kaplan-Moss Owner

Examples for these two attributes would be good; I can see people trying:

class MyUser(AbstractBaseUser):
    email = models.EmailField(...)
    ...
    USERNAME_FIELD = [email]

Which "works" in that it doesn't fail at load-time, but fails later on in really fun and interesting ways.

Russell Keith-Magee Owner

I thought this would be covered by the worked example lower down. However, more examples can't hurt, I suppose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jacob Kaplan-Moss
Owner

@freakboy3742 - other than my inline comments, which fall mostly under the rubric of nit-picking, this looks really fantastic. Great work, and I'm fine with the answer to my comments being "you are high as a kite." +1 on merge.

Mark Lavin mlavin referenced this pull request in mlavin/django-all-access
Closed

Swapable User Support #24

docs/topics/auth.txt
((141 lines not shown))
  1896
+
  1897
+    Returns True if the user account is currently active.
  1898
+
  1899
+.. method:: User.has_perm(perm, obj=None):
  1900
+
  1901
+    Returns True if the user has the named permission. If `obj` is
  1902
+    provided, the permission needs to be checked against a specific object
  1903
+    instance.
  1904
+
  1905
+.. method:: User.has_module_perms(app_label):
  1906
+
  1907
+    Returns True if the user has permission to access models in
  1908
+    the given app.
  1909
+
  1910
+
  1911
+Worked Example
2
Florian Apolloner Owner

Shouldn't that be "Working Example"? I've never heard the term "Worked Example" and even if it's proper English it probably will sound odd for every non-native speaker.

Russell Keith-Magee Owner

"Worked example" is legitimate english; however if it's going to cause confusion, I'll give some thought to an alternate label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/contrib/admin/sites.py
@@ -2,7 +2,7 @@
2 2
 from django.http import Http404, HttpResponseRedirect
3 3
 from django.contrib.admin import ModelAdmin, actions
4 4
 from django.contrib.admin.forms import AdminAuthenticationForm
5  
-from django.contrib.auth import REDIRECT_FIELD_NAME
  5
+from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
2

This new import isn't actually used that i can see.

Russell Keith-Magee Owner

Not sure where this import came from... I've just killed it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Florian Apolloner
Owner

I think we should update https://github.com/freakboy3742/django/blob/5a04cde342cc860384eb844cfda5af55204564ad/django/contrib/auth/models.py#L25 to use .save(update_fields=['last_login']) to prevent unnecessary big update queries (Just seeing this on a project where we store shitloads of data on the user ;)). Thoughts?

Russell Keith-Magee

No argument from me on the principle -- but it's completely orthogonal to this patch. From my perspective, feel free to commit that change as a standalone feature.

Alexey Boriskin uruz commented on the diff
django/contrib/auth/management/commands/createsuperuser.py
((45 lines not shown))
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
4
Alexey Boriskin
uruz added a note

There is the possible clash with the previosly defined options, isn't it? What will happen if we have 'settings' in the REQUIRED_FIELDS?

Russell Keith-Magee Owner

You're correct - this risk does exist. I'm wondering if the right approach is to catch this as a validation issue, and reject any User model with certain required attributes.

Alexey Boriskin
uruz added a note

Other option would be to prefix those dangerous fields with some prefix, like --field-settings or so. Not very consistent though.

Maybe this could solve the problem:

for field in getattr(get_user_model(), 'REQUIRED_FIELDS', []) 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
docs/topics/auth.txt
((33 lines not shown))
  1790
+   User model, you may need to look into using a migration tool like South_
  1791
+   to ease the transition.
  1792
+
  1793
+.. _South: http://south.aeracode.org
  1794
+
  1795
+Referencing the User model
  1796
+--------------------------
  1797
+
  1798
+If you reference :class:`~django.contrib.auth.models.User` directly (for
  1799
+example, by referring to it in a foreign key), your code will not work in
  1800
+projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a
  1801
+different User model.
  1802
+
  1803
+Instead of referring to :class:`~django.contrib.auth.models.User` directly,
  1804
+you should reference the user model using
  1805
+:meth:`~django.contrib.auth.get_user_model()`. This method will return the
2
Ramiro Morales Owner
ramiro added a note

Wouldn't be it better to remove the ~ to leave the full path to django.contrib.auth.get_user_model() visible (we have no API reference docs for it yet)? This would ease the adaptation process for interested readers. Also, it is a function not a method.

Russell Keith-Magee Owner

Good point - I've just made this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/contrib/auth/__init__.py
@@ -86,6 +91,19 @@ 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
+    return get_model(app_label, model_name)
2
Preston Holmes Owner
ptone added a note

if settings.AUTH_USER_MODEL is set to an invalid value that conforms to the pattern ie: "typodapp.nonmodel" then get_model and hence get_user_model will return None.

I'd propose that get_user_model test for None and raise ImproperlyConfigured

Russell Keith-Magee Owner

Good point - I've just implemented this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Preston Holmes ptone commented on the diff
django/contrib/admin/models.py
((11 lines not shown))
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)
3
Preston Holmes Owner
ptone added a note

Would it not be more future proof to have the FK set to get_user_model() ?

The disadvantage is it is somewhat less direct - but would allow more flexibility in future refactors of how the swapped model might be specified (hint hint, wink wink app-loading ;-)

Russell Keith-Magee Owner

This is deliberate -- otherwise we have a 'chicken and egg' situation. In order for get_user_model() to run, you need to have the app cache parsed; but the app cache can't be fully populated until all the models are loaded.

Preston Holmes Owner
ptone added a note

ah yes - I forgot just how much I had changed to actually cook that particular chicken and scramble that particular egg in the app-loading branch. Looks like promoting the convention of explicitly referring to the setting is unavoidable for now then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Florian Apolloner
Owner

The standard auth forms still use the User model instead of get_user_model. Is that on purpose or an oversight?

Russell Keith-Magee

"On purpose" is a little strong, but yes, it's deliberate. The reason is that a making a single auth form adapt to any User model is a complex task. The alternative is asking the user to write their own forms.

The auth forms aren't that complex to write from scratch; perhaps there are some elements we can factor out as utility methods (and suggestions are welcome here), so the overhead associated with maintaining a complex, adaptive form implementation just didn't seem worth it.

Florian Apolloner
Owner

Sry, didn't mean to sound harsh, I just didn't find better English words to express the sentence better. A quick search to auth/views|forms showed two instances where I think changes could be beneficial (I rather have users use our password reset stuff instead of writing their own broken forms).

The first two items are motivated by "making login/logout/pwreset work by default" since they usually don't have strong requirements on the usermodel (eg username-field, email, password, pk should be enough to make them just work)

Russell Keith-Magee

Good catch on the password reset views -- we can certainly clean those up without too much trouble. I've taken a look, and I've got a patch mostly ready.

There is also some extra documentation required here, too. We need to be clear about the requirements for each of these forms -- for example, that the password reset views assume an integer primary key, plus an "email" field that can be used for retrieval, and an "is_active" attribute.

Russell Keith-Magee

Patch applied as 70a0de3

Russell Keith-Magee freakboy3742 closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 41 unique commits by 3 authors.

Jun 04, 2012
Russell Keith-Magee Added model Meta option for swappable models, and made auth.User a sw…
…appable model
7cc0baf
Russell Keith-Magee Modified auth management commands to handle custom user definitions. dabe362
Russell Keith-Magee Modified auth app so that login with alternate auth app is possible. 507bb50
Aug 20, 2012
Russell Keith-Magee Merged recent changes from trunk. 8e3fd70
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 e29c010
Sep 07, 2012
Russell Keith-Magee Merged master changes. 7e82e83
Sep 08, 2012
Thomas Sutton Admin app login form should use swapped user model d088b3a
Thomas Sutton 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.
75118bd
Russell Keith-Magee Added first draft of custom User docs.
Thanks to Greg Turner for the initial text.
e6aaf65
Russell Keith-Magee Added documentation for REQUIRED_FIELDS in custom auth. 40ea8b8
Sep 09, 2012
Russell Keith-Magee Added conditional skips for all tests dependent on the default User m…
…odel
20d1892
Russell Keith-Magee Corrected admin_views tests following removal of the email fallback o…
…n admin logins.
2c5e833
Russell Keith-Magee Merge recent changes from master. 1952656
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 f2ec915
Sep 15, 2012
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 57ac6e3
Russell Keith-Magee Ensure swapped models can't be queried. 5d7bb22
Russell Keith-Magee Added release notes for new swappable User feature. 334cdfc
Russell Keith-Magee Deprecate AUTH_PROFILE_MODULE and get_profile(). 9184972
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 579f152
Anssi Kääriäinen Reworked REQUIRED_FIELDS + create_user() interaction d9f5e5a
Anssi Kääriäinen Splitted User to AbstractUser and User 08bcb4a
Sep 16, 2012
Russell Keith-Magee Added note about backwards incompatible change to admin login messages. b441a6b
Russell Keith-Magee Refactored common 'get' pattern into manager method. 52a02f1
Russell Keith-Magee Refactored skipIfCustomUser into the contrib.auth tests. b550a6d
Russell Keith-Magee Documentation improvements coming from community review. fd8bb4e
Russell Keith-Magee Merge commit '08bcb4aec1ed154cefc631b8510ee13e9af0c19d' into t3011 a9491a8
Russell Keith-Magee Cleanup and documentation of AbstractUser base class. abcb027
Russell Keith-Magee Added protection against proxying swapped models. dfd7213
Russell Keith-Magee Fixes for Python 3 compatibility. dbb3900
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 280bf19
Russell Keith-Magee Added test for proxy model safeguards on swappable models. 913e1ac
Russell Keith-Magee Corrected attribute access on for get_by_natural_key ffd535e
Sep 17, 2012
Russell Keith-Magee Removed some unused imports. 5a04cde
Russell Keith-Magee Improved validation of swappable model settings. 6494bf9
Sep 23, 2012
Russell Keith-Magee Merged recent Django trunk changes. 0229209
Russell Keith-Magee Improved error handling and docs for get_user_model() 98aba85
Russell Keith-Magee Modifications to the handling and docs for auth forms. e2b6e22
Sep 24, 2012
Russell Keith-Magee Ensure sequences are reset correctly in the presence of swapped models. 8a527dd
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 29d1abb
Sep 26, 2012
Russell Keith-Magee Merged recent trunk changes. 531e771
Russell Keith-Magee Merge remote-tracking branch 'django/master' into t3011 d84749a
Something went wrong with that request. Please try again.