Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion django/contrib/auth/management/commands/createsuperuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,13 @@ def username_is_unique(self):
for unique_constraint in self.UserModel._meta.total_unique_constraints
)

@cached_property
def natural_key_defined(self):
return hasattr(self.UserModel._default_manager, "get_by_natural_key")

def _validate_username(self, username, verbose_field_name, database):
"""Validate username. If invalid, return a string error message."""
if self.username_is_unique:
if self.username_is_unique and self.natural_key_defined:
try:
self.UserModel._default_manager.db_manager(database).get_by_natural_key(
username
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
},
"devDependencies": {
"@biomejs/biome": "2.4.15",
"puppeteer": "^25.0.4",
"grunt": "^1.6.2",
"grunt-cli": "^1.5.0",
"grunt-contrib-qunit": "^10.2.0",
Expand Down
2 changes: 2 additions & 0 deletions tests/auth_tests/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .invalid_models import CustomUserNonUniqueUsername
from .is_active import IsActiveTestUser1
from .minimal import MinimalUser
from .no_natural_key import CustomUserNoNaturalKey
from .no_password import NoPasswordUser
from .proxy import Proxy, UserProxy
from .uuid_pk import UUIDUser
Expand All @@ -23,6 +24,7 @@
"CustomPermissionsUser",
"CustomUser",
"CustomUserCompositePrimaryKey",
"CustomUserNoNaturalKey",
"CustomUserNonUniqueUsername",
"CustomUserWithFK",
"CustomUserWithM2M",
Expand Down
22 changes: 22 additions & 0 deletions tests/auth_tests/models/no_natural_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib.auth.hashers import make_password
from django.db import models


class CustomUserNoNaturalKeyManager(models.Manager):
def create_superuser(self, email, password=None, **extra_fields):
user = self.model(email=email, is_superuser=True, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user


class CustomUserNoNaturalKey(models.Model):
email = models.EmailField(max_length=255, unique=True)
password = models.CharField(max_length=128)
is_superuser = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)

USERNAME_FIELD = "email"
REQUIRED_FIELDS = []

objects = CustomUserNoNaturalKeyManager()
62 changes: 62 additions & 0 deletions tests/auth_tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
from django.core.management import call_command
from django.core.management.base import CommandError
from django.db import migrations
from django.db.utils import IntegrityError
from django.test import TestCase, override_settings
from django.test.testcases import TransactionTestCase
from django.utils.translation import gettext_lazy as _

from .models import (
CustomUser,
CustomUserNoNaturalKey,
CustomUserNonUniqueUsername,
CustomUserWithFK,
CustomUserWithM2M,
Expand Down Expand Up @@ -470,6 +472,66 @@ def createsuperuser():
users = CustomUserNonUniqueUsername.objects.filter(username="joe")
self.assertEqual(users.count(), 2)

@override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey")
def test_swappable_user_no_natural_key_non_interactive(self):
new_io = StringIO()
call_command(
"createsuperuser",
interactive=False,
email="joe@somewhere.org",
stdout=new_io,
)
command_output = new_io.getvalue().strip()
self.assertEqual(command_output, "Superuser created successfully.")

user = CustomUserNoNaturalKey._default_manager.get(email="joe@somewhere.org")
self.assertIsNotNone(user)

@override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey")
def test_swappable_user_no_natural_key_interactive(self):
@mock_inputs(
{
"Email: ": "joe@somewhere.org",
"password": "nopasswd",
}
)
def createsuperuser():
new_io = StringIO()
call_command(
"createsuperuser",
interactive=True,
stdout=new_io,
stdin=MockTTY(),
)
command_output = new_io.getvalue().strip()
self.assertEqual(command_output, "Superuser created successfully.")

createsuperuser()
user = CustomUserNoNaturalKey._default_manager.get(email="joe@somewhere.org")
self.assertIsNotNone(user)

@override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey")
def test_swappable_user_no_natural_key_duplicate_allowed(self):
"""Without get_by_natural_key,
duplicate username validation is skipped."""
new_io = StringIO()
call_command(
"createsuperuser",
interactive=False,
email="joe@somewhere.org",
stdout=new_io,
)
# Creating a second user with the same email won't be caught by
# _validate_username since there's no get_by_natural_key; it will
# fail at the database level instead due to the unique constraint.
with self.assertRaises(IntegrityError):
call_command(
"createsuperuser",
interactive=False,
email="joe@somewhere.org",
stdout=new_io,
)

def test_skip_if_not_in_TTY(self):
"""
If the command is not called from a TTY, it should be skipped and a
Expand Down
Loading