Browse Source

Fixed #33613 -- Made createsuperuser detect uniqueness of USERNAME_FIELD when using Meta.constraints.

Lucidiot 3 years ago
parent
commit
13a9cde133

+ 14 - 1
django/contrib/auth/management/commands/createsuperuser.py

@@ -11,6 +11,7 @@ from django.contrib.auth.password_validation import validate_password
 from django.core import exceptions
 from django.core.management.base import BaseCommand, CommandError
 from django.db import DEFAULT_DB_ALIAS
+from django.utils.functional import cached_property
 from django.utils.text import capfirst
 
 
@@ -277,9 +278,21 @@ class Command(BaseCommand):
             else "",
         )
 
+    @cached_property
+    def username_is_unique(self):
+        if self.username_field.unique:
+            return True
+        for unique_constraint in self.UserModel._meta.total_unique_constraints:
+            if (
+                len(unique_constraint.fields) == 1
+                and unique_constraint.fields[0] == self.username_field.name
+            ):
+                return True
+        return False
+
     def _validate_username(self, username, verbose_field_name, database):
         """Validate username. If invalid, return a string error message."""
-        if self.username_field.unique:
+        if self.username_is_unique:
             try:
                 self.UserModel._default_manager.db_manager(database).get_by_natural_key(
                     username

+ 1 - 1
docs/topics/auth/customizing.txt

@@ -548,7 +548,7 @@ password resets. You must then provide some key implementation details:
         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
+        identifier. The field *must* be unique (e.g. have ``unique=True`` set
         in its definition), unless you use a custom authentication backend that
         can support non-unique usernames.
 

+ 2 - 0
tests/auth_tests/models/__init__.py

@@ -11,6 +11,7 @@ from .with_foreign_key import CustomUserWithFK, Email
 from .with_integer_username import IntegerUsernameUser
 from .with_last_login_attr import UserWithDisabledLastLoginField
 from .with_many_to_many import CustomUserWithM2M, CustomUserWithM2MThrough, Organization
+from .with_unique_constraint import CustomUserWithUniqueConstraint
 
 __all__ = (
     "CustomEmailField",
@@ -20,6 +21,7 @@ __all__ = (
     "CustomUserWithFK",
     "CustomUserWithM2M",
     "CustomUserWithM2MThrough",
+    "CustomUserWithUniqueConstraint",
     "CustomUserWithoutIsActiveField",
     "Email",
     "ExtensionUser",

+ 22 - 0
tests/auth_tests/models/with_unique_constraint.py

@@ -0,0 +1,22 @@
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
+from django.db import models
+
+
+class CustomUserWithUniqueConstraintManager(BaseUserManager):
+    def create_superuser(self, username, password):
+        user = self.model(username=username)
+        user.set_password(password)
+        user.save(using=self._db)
+        return user
+
+
+class CustomUserWithUniqueConstraint(AbstractBaseUser):
+    username = models.CharField(max_length=150)
+
+    objects = CustomUserWithUniqueConstraintManager()
+    USERNAME_FIELD = "username"
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(fields=["username"], name="unique_custom_username"),
+        ]

+ 36 - 0
tests/auth_tests/test_management.py

@@ -23,6 +23,7 @@ from .models import (
     CustomUserNonUniqueUsername,
     CustomUserWithFK,
     CustomUserWithM2M,
+    CustomUserWithUniqueConstraint,
     Email,
     Organization,
     UserProxy,
@@ -1065,6 +1066,41 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
 
         test(self)
 
+    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithUniqueConstraint")
+    def test_existing_username_meta_unique_constraint(self):
+        """
+        Creation fails if the username already exists and a custom user model
+        has UniqueConstraint.
+        """
+        user = CustomUserWithUniqueConstraint.objects.create(username="janet")
+        new_io = StringIO()
+        entered_passwords = ["password", "password"]
+        # Enter the existing username first and then a new one.
+        entered_usernames = [user.username, "joe"]
+
+        def return_passwords():
+            return entered_passwords.pop(0)
+
+        def return_usernames():
+            return entered_usernames.pop(0)
+
+        @mock_inputs({"password": return_passwords, "username": return_usernames})
+        def test(self):
+            call_command(
+                "createsuperuser",
+                interactive=True,
+                stdin=MockTTY(),
+                stdout=new_io,
+                stderr=new_io,
+            )
+            self.assertEqual(
+                new_io.getvalue().strip(),
+                "Error: That username is already taken.\n"
+                "Superuser created successfully.",
+            )
+
+        test(self)
+
     def test_existing_username_non_interactive(self):
         """Creation fails if the username already exists."""
         User.objects.create(username="janet")