From 13a9cde133ac82e33dd091ca9bb9c677804afbe1 Mon Sep 17 00:00:00 2001 From: Lucidiot Date: Thu, 31 Mar 2022 14:39:28 +0200 Subject: [PATCH] Fixed #33613 -- Made createsuperuser detect uniqueness of USERNAME_FIELD when using Meta.constraints. --- .../management/commands/createsuperuser.py | 15 +++++++- docs/topics/auth/customizing.txt | 2 +- tests/auth_tests/models/__init__.py | 2 ++ .../models/with_unique_constraint.py | 22 ++++++++++++ tests/auth_tests/test_management.py | 36 +++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/auth_tests/models/with_unique_constraint.py diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 5fffa55a226..314c5151c4a 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/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 diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index ca1c39bd8e8..382fc94a6be 100644 --- a/docs/topics/auth/customizing.txt +++ b/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. diff --git a/tests/auth_tests/models/__init__.py b/tests/auth_tests/models/__init__.py index bce5edf8839..ed0647b90d8 100644 --- a/tests/auth_tests/models/__init__.py +++ b/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", diff --git a/tests/auth_tests/models/with_unique_constraint.py b/tests/auth_tests/models/with_unique_constraint.py new file mode 100644 index 00000000000..fa8dc8f1f06 --- /dev/null +++ b/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"), + ] diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index 071ea85a65d..2e82c1bb144 100644 --- a/tests/auth_tests/test_management.py +++ b/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")