From 1ea87c8c7974acb5fa795362253c61b38f3cb137 Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Fri, 5 Jun 2015 14:33:04 +0100 Subject: [PATCH] Fixed #24910 -- Added createsuperuser support for non-unique USERNAME_FIELDs Clarified docs to say that a non-unique USERNAME_FIELD is permissable as long as the custom auth backend can support it. --- .../management/commands/createsuperuser.py | 16 +++++------ docs/topics/auth/customizing.txt | 17 +++++++----- tests/auth_tests/models/invalid_models.py | 15 +++++++++-- tests/auth_tests/test_management.py | 27 +++++++++++++++++++ 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 7d374b863d9..1e3b9a8d7c5 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -101,14 +101,14 @@ class Command(BaseCommand): username = self.get_input_data(self.username_field, input_msg, default_username) if not username: continue - try: - self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) - except self.UserModel.DoesNotExist: - pass - else: - self.stderr.write("Error: That %s is already taken." % - verbose_field_name) - username = None + if self.username_field.unique: + try: + self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) + except self.UserModel.DoesNotExist: + pass + else: + self.stderr.write("Error: That %s is already taken." % verbose_field_name) + username = None for field_name in self.UserModel.REQUIRED_FIELDS: field = self.UserModel._meta.get_field(field_name) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 6d06ef6e25a..7745574d5d4 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -477,9 +477,11 @@ Specifying a custom User model Django expects your custom User model to meet some minimum requirements. -#. 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. +#. If you use the default authentication backend, then 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. A non-unique + username field is allowed if you use a custom authentication backend that + can support it. #. 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 @@ -506,10 +508,11 @@ password resets. You must then provide some key implementation details: .. attribute:: 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. The field *must* be unique (i.e., have ``unique=True`` - set in its definition). + 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. The field *must* be unique (i.e., have ``unique=True`` set + in its definition), unless you use a custom authentication backend that + can support non-unique usernames. In the following example, the field ``identifier`` is used as the identifying field:: diff --git a/tests/auth_tests/models/invalid_models.py b/tests/auth_tests/models/invalid_models.py index 46b4819fa45..eaaff2aa15f 100644 --- a/tests/auth_tests/models/invalid_models.py +++ b/tests/auth_tests/models/invalid_models.py @@ -1,12 +1,23 @@ -from django.contrib.auth.models import AbstractBaseUser +from django.contrib.auth.models import AbstractBaseUser, UserManager from django.db import models class CustomUserNonUniqueUsername(AbstractBaseUser): - "A user with a non-unique username" + """ + A user with a non-unique username. + + This model is not invalid if it is used with a custom authentication + backend which supports non-unique usernames. + """ username = models.CharField(max_length=30) + email = models.EmailField(blank=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + objects = UserManager() class Meta: app_label = 'auth' diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index e9c61054966..f45703fcfd8 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -305,6 +305,33 @@ class CreatesuperuserManagementCommandTestCase(TestCase): self.assertEqual(CustomUser._default_manager.count(), 0) + @override_settings( + AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername', + AUTHENTICATION_BACKENDS=['my.custom.backend'], + ) + def test_swappable_user_username_non_unique(self): + @mock_inputs({ + 'username': 'joe', + 'password': 'nopasswd', + }) + def createsuperuser(): + new_io = six.StringIO() + call_command( + "createsuperuser", + interactive=True, + email="joe@somewhere.org", + stdout=new_io, + stdin=MockTTY(), + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + + for i in range(2): + createsuperuser() + + users = CustomUserNonUniqueUsername.objects.filter(username="joe") + self.assertEqual(users.count(), 2) + def test_skip_if_not_in_TTY(self): """ If the command is not called from a TTY, it should be skipped and a