summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucidiot <lucidiot@brainshit.fr>2022-03-31 14:39:28 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-01 11:39:41 +0200
commit13a9cde133ac82e33dd091ca9bb9c677804afbe1 (patch)
tree1b85c8c9e75d3aada9e513d6a0cf8e8347b5b82b
parentae506181f7fb9d9e74f4935686540bef29b60255 (diff)
Fixed #33613 -- Made createsuperuser detect uniqueness of USERNAME_FIELD when using Meta.constraints.
-rw-r--r--django/contrib/auth/management/commands/createsuperuser.py15
-rw-r--r--docs/topics/auth/customizing.txt2
-rw-r--r--tests/auth_tests/models/__init__.py2
-rw-r--r--tests/auth_tests/models/with_unique_constraint.py22
-rw-r--r--tests/auth_tests/test_management.py36
5 files changed, 75 insertions, 2 deletions
diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py
index 5fffa55a22..314c5151c4 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 ca1c39bd8e..382fc94a6b 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 bce5edf883..ed0647b90d 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 0000000000..fa8dc8f1f0
--- /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 071ea85a65..2e82c1bb14 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")