mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #25617 -- Added case-insensitive unique username validation in UserCreationForm.
Co-Authored-By: Neven Mundar <nmundar@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							1833eb3f3e
						
					
				
				
					commit
					298d02a77a
				
			| @@ -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"), | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
|             "<ul><li>" | ||||
| @@ -313,10 +314,12 @@ class UserCreationFormTest(TestDataMixin, TestCase): | ||||
|         ] | ||||
|     ) | ||||
|     def test_user_create_form_validates_password_with_all_data(self): | ||||
|         """UserCreationForm password validation uses all of the form's data.""" | ||||
|         """ | ||||
|         BaseUserCreationForm password validation uses all of the form's data. | ||||
|         """ | ||||
|  | ||||
|         class CustomUserCreationForm(UserCreationForm): | ||||
|             class Meta(UserCreationForm.Meta): | ||||
|         class CustomUserCreationForm(BaseUserCreationForm): | ||||
|             class Meta(BaseUserCreationForm.Meta): | ||||
|                 model = User | ||||
|                 fields = ("username", "email", "first_name", "last_name") | ||||
|  | ||||
| @@ -336,13 +339,13 @@ class UserCreationFormTest(TestDataMixin, TestCase): | ||||
|         ) | ||||
|  | ||||
|     def test_username_field_autocapitalize_none(self): | ||||
|         form = UserCreationForm() | ||||
|         form = BaseUserCreationForm() | ||||
|         self.assertEqual( | ||||
|             form.fields["username"].widget.attrs.get("autocapitalize"), "none" | ||||
|         ) | ||||
|  | ||||
|     def test_html_autocomplete_attributes(self): | ||||
|         form = UserCreationForm() | ||||
|         form = BaseUserCreationForm() | ||||
|         tests = ( | ||||
|             ("username", "username"), | ||||
|             ("password1", "new-password"), | ||||
| @@ -355,6 +358,21 @@ class UserCreationFormTest(TestDataMixin, TestCase): | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| class UserCreationFormTest(TestDataMixin, TestCase): | ||||
|     def test_case_insensitive_username(self): | ||||
|         data = { | ||||
|             "username": "TeStClIeNt", | ||||
|             "password1": "test123", | ||||
|             "password2": "test123", | ||||
|         } | ||||
|         form = UserCreationForm(data) | ||||
|         self.assertFalse(form.is_valid()) | ||||
|         self.assertEqual( | ||||
|             form["username"].errors, | ||||
|             ["A user with that username already exists."], | ||||
|         ) | ||||
|  | ||||
|  | ||||
| # To verify that the login form rejects inactive users, use an authentication | ||||
| # backend that allows them. | ||||
| @override_settings( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user