Browse Source

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.
Alasdair Nicol 9 years ago
parent
commit
1ea87c8c79

+ 8 - 8
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)

+ 10 - 7
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::

+ 13 - 2
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'

+ 27 - 0
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