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

This commit is contained in:
Lucidiot 2022-03-31 14:39:28 +02:00 committed by Mariusz Felisiak
parent ae506181f7
commit 13a9cde133
5 changed files with 75 additions and 2 deletions

View File

@ -11,6 +11,7 @@ from django.contrib.auth.password_validation import validate_password
from django.core import exceptions from django.core import exceptions
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from django.utils.functional import cached_property
from django.utils.text import capfirst from django.utils.text import capfirst
@ -277,9 +278,21 @@ class Command(BaseCommand):
else "", 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): def _validate_username(self, username, verbose_field_name, database):
"""Validate username. If invalid, return a string error message.""" """Validate username. If invalid, return a string error message."""
if self.username_field.unique: if self.username_is_unique:
try: try:
self.UserModel._default_manager.db_manager(database).get_by_natural_key( self.UserModel._default_manager.db_manager(database).get_by_natural_key(
username username

View File

@ -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 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 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 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 in its definition), unless you use a custom authentication backend that
can support non-unique usernames. can support non-unique usernames.

View File

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

View File

@ -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"),
]

View File

@ -23,6 +23,7 @@ from .models import (
CustomUserNonUniqueUsername, CustomUserNonUniqueUsername,
CustomUserWithFK, CustomUserWithFK,
CustomUserWithM2M, CustomUserWithM2M,
CustomUserWithUniqueConstraint,
Email, Email,
Organization, Organization,
UserProxy, UserProxy,
@ -1065,6 +1066,41 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
test(self) 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): def test_existing_username_non_interactive(self):
"""Creation fails if the username already exists.""" """Creation fails if the username already exists."""
User.objects.create(username="janet") User.objects.create(username="janet")