diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 6b9d2dd7b8..dda6a07f02 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -7,6 +7,7 @@ from django.contrib.auth import ( from django.contrib.auth.hashers import ( UNUSABLE_PASSWORD_PREFIX, identify_hasher, ) +from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site from django.core.mail import EmailMultiAlternatives @@ -82,9 +83,9 @@ class UserCreationForm(forms.ModelForm): ) class Meta: - model = UserModel - fields = (UserModel.USERNAME_FIELD,) - field_classes = {UserModel.USERNAME_FIELD: UsernameField} + model = User + fields = ("username",) + field_classes = {'username': UsernameField} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -131,9 +132,9 @@ class UserChangeForm(forms.ModelForm): ) class Meta: - model = UserModel + model = User fields = '__all__' - field_classes = {UserModel.USERNAME_FIELD: UsernameField} + field_classes = {'username': UsernameField} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 67e91b2e54..676aa56d39 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -814,20 +814,11 @@ are working with. The following forms are compatible with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`: -* :class:`~django.contrib.auth.forms.AuthenticationForm` +* :class:`~django.contrib.auth.forms.AuthenticationForm`: Uses the username + field specified by :attr:`~models.CustomUser.USERNAME_FIELD`. * :class:`~django.contrib.auth.forms.SetPasswordForm` * :class:`~django.contrib.auth.forms.PasswordChangeForm` * :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` -* :class:`~django.contrib.auth.forms.UserCreationForm` -* :class:`~django.contrib.auth.forms.UserChangeForm` - -The forms that handle a username use the username field specified by -:attr:`~models.CustomUser.USERNAME_FIELD`. - -.. versionchanged:: 2.1 - - In older versions, ``UserCreationForm`` and ``UserChangeForm`` need to be - rewritten to work with custom user models. The following forms make assumptions about the user model and can be used as-is if those assumptions are met: @@ -838,6 +829,25 @@ if those assumptions are met: default) that can be used to identify the user and a boolean field named ``is_active`` to prevent password resets for inactive users. +Finally, the following forms are tied to +:class:`~django.contrib.auth.models.User` and need to be rewritten or extended +to work with a custom user model: + +* :class:`~django.contrib.auth.forms.UserCreationForm` +* :class:`~django.contrib.auth.forms.UserChangeForm` + +If your custom user model is a simple subclass of ``AbstractUser``, then you +can extend these forms in this manner:: + + from django.contrib.auth.forms import UserCreationForm + from myapp.models import CustomUser + + class CustomUserCreationForm(UserCreationForm): + + class Meta(UserCreationForm.Meta): + model = CustomUser + fields = UserCreationForm.Meta.fields + ('custom_field',) + Custom users and :mod:`django.contrib.admin` -------------------------------------------- diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 94a8fc592f..4fcd465d89 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -1508,12 +1508,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: A :class:`~django.forms.ModelForm` for creating a new user. - It has three fields: one named after the - :attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD` from the - user model, and ``password1`` and ``password2``. - - It verifies that ``password1`` and ``password2`` match, validates the - password using + It has three fields: ``username`` (from the user model), ``password1``, + and ``password2``. It verifies that ``password1`` and ``password2`` match, + validates the password using :func:`~django.contrib.auth.password_validation.validate_password`, and sets the user's password using :meth:`~django.contrib.auth.models.User.set_password()`. diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 52d61cfe87..825138755d 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -1,9 +1,7 @@ import datetime import re -from importlib import reload from unittest import mock -import django from django import forms from django.contrib.auth.forms import ( AdminPasswordChangeForm, AuthenticationForm, PasswordChangeForm, @@ -13,7 +11,7 @@ from django.contrib.auth.forms import ( from django.contrib.auth.models import User from django.contrib.auth.signals import user_login_failed from django.contrib.sites.models import Site -from django.core import mail, signals +from django.core import mail from django.core.mail import EmailMultiAlternatives from django.forms.fields import CharField, Field, IntegerField from django.test import SimpleTestCase, TestCase, override_settings @@ -29,24 +27,6 @@ from .models.with_integer_username import IntegerUsernameUser from .settings import AUTH_TEMPLATES -def reload_auth_forms(sender, setting, value, enter, **kwargs): - if setting == 'AUTH_USER_MODEL': - reload(django.contrib.auth.forms) - - -class ReloadFormsMixin: - - @classmethod - def setUpClass(cls): - super().setUpClass() - signals.setting_changed.connect(reload_auth_forms) - - @classmethod - def tearDownClass(cls): - signals.setting_changed.disconnect(reload_auth_forms) - super().tearDownClass() - - class TestDataMixin: @classmethod @@ -57,10 +37,9 @@ class TestDataMixin: cls.u4 = User.objects.create(username='empty_password', password='') cls.u5 = User.objects.create(username='unmanageable_password', password='$') cls.u6 = User.objects.create(username='unknown_password', password='foo$bar') - cls.u7 = ExtensionUser.objects.create(username='extension_client', date_of_birth='1998-02-24') -class UserCreationFormTest(ReloadFormsMixin, TestDataMixin, TestCase): +class UserCreationFormTest(TestDataMixin, TestCase): def test_user_already_exists(self): data = { @@ -196,25 +175,19 @@ class UserCreationFormTest(ReloadFormsMixin, TestDataMixin, TestCase): ) def test_custom_form(self): - with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'): - from django.contrib.auth.forms import UserCreationForm - self.assertEqual(UserCreationForm.Meta.model, ExtensionUser) + class CustomUserCreationForm(UserCreationForm): + class Meta(UserCreationForm.Meta): + model = ExtensionUser + fields = UserCreationForm.Meta.fields + ('date_of_birth',) - class CustomUserCreationForm(UserCreationForm): - class Meta(UserCreationForm.Meta): - fields = UserCreationForm.Meta.fields + ('date_of_birth',) - - data = { - 'username': 'testclient', - 'password1': 'testclient', - 'password2': 'testclient', - 'date_of_birth': '1988-02-24', - } - form = CustomUserCreationForm(data) - self.assertTrue(form.is_valid()) - # reload_auth_forms() reloads the form. - from django.contrib.auth.forms import UserCreationForm - self.assertEqual(UserCreationForm.Meta.model, User) + data = { + 'username': 'testclient', + 'password1': 'testclient', + 'password2': 'testclient', + 'date_of_birth': '1988-02-24', + } + form = CustomUserCreationForm(data) + self.assertTrue(form.is_valid()) def test_custom_form_with_different_username_field(self): class CustomUserCreationForm(UserCreationForm): @@ -288,30 +261,6 @@ class UserCreationFormTest(ReloadFormsMixin, TestDataMixin, TestCase): ['The password is too similar to the first name.'], ) - def test_with_custom_user_model(self): - with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'): - data = { - 'username': 'test_username', - 'password1': 'test_password', - 'password2': 'test_password', - } - from django.contrib.auth.forms import UserCreationForm - self.assertEqual(UserCreationForm.Meta.model, ExtensionUser) - form = UserCreationForm(data) - self.assertTrue(form.is_valid()) - - def test_customer_user_model_with_different_username_field(self): - with override_settings(AUTH_USER_MODEL='auth_tests.CustomUser'): - from django.contrib.auth.forms import UserCreationForm - self.assertEqual(UserCreationForm.Meta.model, CustomUser) - data = { - 'email': 'testchange@test.com', - 'password1': 'test_password', - 'password2': 'test_password', - } - form = UserCreationForm(data) - self.assertTrue(form.is_valid()) - # To verify that the login form rejects inactive users, use an authentication # backend that allows them. @@ -677,7 +626,7 @@ class PasswordChangeFormTest(TestDataMixin, TestCase): self.assertEqual(form.cleaned_data['new_password2'], data['new_password2']) -class UserChangeFormTest(ReloadFormsMixin, TestDataMixin, TestCase): +class UserChangeFormTest(TestDataMixin, TestCase): def test_username_validity(self): user = User.objects.get(username='testclient') @@ -751,51 +700,22 @@ class UserChangeFormTest(ReloadFormsMixin, TestDataMixin, TestCase): self.assertEqual(form.initial['password'], form['password'].value()) def test_custom_form(self): - with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'): - from django.contrib.auth.forms import UserChangeForm - self.assertEqual(UserChangeForm.Meta.model, ExtensionUser) + class CustomUserChangeForm(UserChangeForm): + class Meta(UserChangeForm.Meta): + model = ExtensionUser + fields = ('username', 'password', 'date_of_birth',) - class CustomUserChangeForm(UserChangeForm): - class Meta(UserChangeForm.Meta): - fields = ('username', 'password', 'date_of_birth') - - data = { - 'username': 'testclient', - 'password': 'testclient', - 'date_of_birth': '1998-02-24', - } - form = CustomUserChangeForm(data, instance=self.u7) - self.assertTrue(form.is_valid()) - form.save() - self.assertEqual(form.cleaned_data['username'], 'testclient') - self.assertEqual(form.cleaned_data['date_of_birth'], datetime.date(1998, 2, 24)) - # reload_auth_forms() reloads the form. - from django.contrib.auth.forms import UserChangeForm - self.assertEqual(UserChangeForm.Meta.model, User) - - def test_with_custom_user_model(self): - with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'): - from django.contrib.auth.forms import UserChangeForm - self.assertEqual(UserChangeForm.Meta.model, ExtensionUser) - data = { - 'username': 'testclient', - 'date_joined': '1998-02-24', - 'date_of_birth': '1998-02-24', - } - form = UserChangeForm(data, instance=self.u7) - self.assertTrue(form.is_valid()) - - def test_customer_user_model_with_different_username_field(self): - with override_settings(AUTH_USER_MODEL='auth_tests.CustomUser'): - from django.contrib.auth.forms import UserChangeForm - self.assertEqual(UserChangeForm.Meta.model, CustomUser) - user = CustomUser.custom_objects.create(email='test@test.com', date_of_birth='1998-02-24') - data = { - 'email': 'testchange@test.com', - 'date_of_birth': '1998-02-24', - } - form = UserChangeForm(data, instance=user) - self.assertTrue(form.is_valid()) + user = User.objects.get(username='testclient') + data = { + 'username': 'testclient', + 'password': 'testclient', + 'date_of_birth': '1998-02-24', + } + form = CustomUserChangeForm(data, instance=user) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(form.cleaned_data['username'], 'testclient') + self.assertEqual(form.cleaned_data['date_of_birth'], datetime.date(1998, 2, 24)) def test_password_excluded(self): class UserChangeFormWithoutPassword(UserChangeForm):