diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 63420af94e2..0376d177094 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -81,7 +81,7 @@ class UsernameField(forms.CharField): } -class UserCreationForm(forms.ModelForm): +class BaseUserCreationForm(forms.ModelForm): """ A form that creates a user, with no privileges, from the given username and password. @@ -146,6 +146,21 @@ class UserCreationForm(forms.ModelForm): return user +class UserCreationForm(BaseUserCreationForm): + error_messages = { + **BaseUserCreationForm.error_messages, + "unique": _("A user with that username already exists."), + } + + def clean_username(self): + """Reject usernames that differ only in case.""" + username = self.cleaned_data.get("username") + if username and User.objects.filter(username__iexact=username).exists(): + raise forms.ValidationError(self.error_messages["unique"], code="unique") + else: + return username + + class UserChangeForm(forms.ModelForm): password = ReadOnlyPasswordHashField( label=_("Password"), diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index b5a7e61f6f2..826545f4445 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -127,6 +127,9 @@ Minor features * :class:`~django.contrib.auth.forms.UserCreationForm` now saves many-to-many form fields for a custom user model. +* The new :class:`~django.contrib.auth.forms.BaseUserCreationForm` is now the + recommended base class for customizing the user creation form. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -484,6 +487,10 @@ Miscellaneous * The minimum supported version of ``asgiref`` is increased from 3.5.2 to 3.6.0. +* :class:`~django.contrib.auth.forms.UserCreationForm` now rejects usernames + that differ only in case. If you need the previous behavior, use + :class:`~django.contrib.auth.forms.BaseUserCreationForm` instead. + .. _deprecated-features-4.2: Features deprecated in 4.2 diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 549430aaadb..038f2b8eaf6 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -1654,9 +1654,12 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: A form used in the admin interface to change a user's information and permissions. -.. class:: UserCreationForm +.. class:: BaseUserCreationForm - A :class:`~django.forms.ModelForm` for creating a new user. + .. versionadded:: 4.2 + + A :class:`~django.forms.ModelForm` for creating a new user. This is the + recommended base class if you need to customize the user creation form. It has three fields: ``username`` (from the user model), ``password1``, and ``password2``. It verifies that ``password1`` and ``password2`` match, @@ -1665,11 +1668,19 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: sets the user's password using :meth:`~django.contrib.auth.models.User.set_password()`. +.. class:: UserCreationForm + + Inherits from :class:`BaseUserCreationForm`. To help prevent confusion with + similar usernames, the form doesn't allow usernames that differ only in + case. + .. versionchanged:: 4.2 In older versions, :class:`UserCreationForm` didn't save many-to-many form fields for a custom user model. + In older versions, usernames that differ only in case are allowed. + .. currentmodule:: django.contrib.auth Authentication data in templates diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 1aee923bb90..c3ce1f570ff 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -6,6 +6,7 @@ from unittest import mock from django.contrib.auth.forms import ( AdminPasswordChangeForm, AuthenticationForm, + BaseUserCreationForm, PasswordChangeForm, PasswordResetForm, ReadOnlyPasswordHashField, @@ -54,14 +55,14 @@ class TestDataMixin: cls.u6 = User.objects.create(username="unknown_password", password="foo$bar") -class UserCreationFormTest(TestDataMixin, TestCase): +class BaseUserCreationFormTest(TestDataMixin, TestCase): def test_user_already_exists(self): data = { "username": "testclient", "password1": "test123", "password2": "test123", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual( form["username"].errors, @@ -74,7 +75,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "test123", "password2": "test123", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertFalse(form.is_valid()) validator = next( v @@ -90,7 +91,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "test123", "password2": "test", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual( form["password2"].errors, [str(form.error_messages["password_mismatch"])] @@ -99,7 +100,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): def test_both_passwords(self): # One (or both) passwords weren't given data = {"username": "jsmith"} - form = UserCreationForm(data) + form = BaseUserCreationForm(data) required_error = [str(Field.default_error_messages["required"])] self.assertFalse(form.is_valid()) self.assertEqual(form["password1"].errors, required_error) @@ -119,7 +120,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "test123", "password2": "test123", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertTrue(form.is_valid()) form.save(commit=False) self.assertEqual(password_changed.call_count, 0) @@ -133,7 +134,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "test123", "password2": "test123", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertTrue(form.is_valid()) u = form.save() self.assertEqual(u.username, "宝") @@ -147,7 +148,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "pwd2", "password2": "pwd2", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertTrue(form.is_valid()) user = form.save() self.assertNotEqual(user.username, ohm_username) @@ -168,7 +169,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "pwd2", "password2": "pwd2", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual( form.errors["username"], ["A user with that username already exists."] @@ -198,7 +199,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": "testclient", "password2": "testclient", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual(len(form["password2"].errors), 2) self.assertIn( @@ -210,8 +211,8 @@ class UserCreationFormTest(TestDataMixin, TestCase): ) def test_custom_form(self): - class CustomUserCreationForm(UserCreationForm): - class Meta(UserCreationForm.Meta): + class CustomUserCreationForm(BaseUserCreationForm): + class Meta(BaseUserCreationForm.Meta): model = ExtensionUser fields = UserCreationForm.Meta.fields + ("date_of_birth",) @@ -225,8 +226,8 @@ class UserCreationFormTest(TestDataMixin, TestCase): self.assertTrue(form.is_valid()) def test_custom_form_with_different_username_field(self): - class CustomUserCreationForm(UserCreationForm): - class Meta(UserCreationForm.Meta): + class CustomUserCreationForm(BaseUserCreationForm): + class Meta(BaseUserCreationForm.Meta): model = CustomUser fields = ("email", "date_of_birth") @@ -240,8 +241,8 @@ class UserCreationFormTest(TestDataMixin, TestCase): self.assertTrue(form.is_valid()) def test_custom_form_hidden_username_field(self): - class CustomUserCreationForm(UserCreationForm): - class Meta(UserCreationForm.Meta): + class CustomUserCreationForm(BaseUserCreationForm): + class Meta(BaseUserCreationForm.Meta): model = CustomUserWithoutIsActiveField fields = ("email",) # without USERNAME_FIELD @@ -254,8 +255,8 @@ class UserCreationFormTest(TestDataMixin, TestCase): self.assertTrue(form.is_valid()) def test_custom_form_saves_many_to_many_field(self): - class CustomUserCreationForm(UserCreationForm): - class Meta(UserCreationForm.Meta): + class CustomUserCreationForm(BaseUserCreationForm): + class Meta(BaseUserCreationForm.Meta): model = CustomUserWithM2M fields = UserCreationForm.Meta.fields + ("orgs",) @@ -278,7 +279,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): "password1": " testpassword ", "password2": " testpassword ", } - form = UserCreationForm(data) + form = BaseUserCreationForm(data) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data["password1"], data["password1"]) self.assertEqual(form.cleaned_data["password2"], data["password2"]) @@ -294,7 +295,7 @@ class UserCreationFormTest(TestDataMixin, TestCase): ] ) def test_password_help_text(self): - form = UserCreationForm() + form = BaseUserCreationForm() self.assertEqual( form.fields["password1"].help_text, "