Browse Source

Fixed #28757 -- Allowed using contrib.auth forms without installing contrib.auth.

Also fixed #28608 -- Allowed UserCreationForm and UserChangeForm to
work with custom user models.

Thanks Sagar Chalise and Rômulo Collopy for reports, and Tim Graham
and Tim Martin for reviews.
shanghui 7 years ago
parent
commit
3333d935d2

+ 5 - 6
django/contrib/auth/forms.py

@@ -7,7 +7,6 @@ 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
@@ -83,9 +82,9 @@ class UserCreationForm(forms.ModelForm):
     )
 
     class Meta:
-        model = User
-        fields = ("username",)
-        field_classes = {'username': UsernameField}
+        model = UserModel
+        fields = (UserModel.USERNAME_FIELD,)
+        field_classes = {UserModel.USERNAME_FIELD: UsernameField}
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -132,9 +131,9 @@ class UserChangeForm(forms.ModelForm):
     )
 
     class Meta:
-        model = User
+        model = UserModel
         fields = '__all__'
-        field_classes = {'username': UsernameField}
+        field_classes = {UserModel.USERNAME_FIELD: UsernameField}
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)

+ 4 - 0
docs/releases/2.1.txt

@@ -48,6 +48,10 @@ Minor features
 * :djadmin:`createsuperuser` now gives a prompt to allow bypassing the
   :setting:`AUTH_PASSWORD_VALIDATORS` checks.
 
+* :class:`~django.contrib.auth.forms.UserCreationForm` and
+  :class:`~django.contrib.auth.forms.UserChangeForm` no longer need to be
+  rewritten for a custom user model.
+
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 11 - 21
docs/topics/auth/customizing.txt

@@ -820,11 +820,20 @@ are working with.
 The following forms are compatible with any subclass of
 :class:`~django.contrib.auth.models.AbstractBaseUser`:
 
-* :class:`~django.contrib.auth.forms.AuthenticationForm`: Uses the username
-  field specified by :attr:`~models.CustomUser.USERNAME_FIELD`.
+* :class:`~django.contrib.auth.forms.AuthenticationForm`
 * :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:
@@ -835,25 +844,6 @@ 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`
 --------------------------------------------
 

+ 6 - 3
docs/topics/auth/default.txt

@@ -1486,9 +1486,12 @@ 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: ``username`` (from the user model), ``password1``,
-    and ``password2``. It verifies that ``password1`` and ``password2`` match,
-    validates the password using
+    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
     :func:`~django.contrib.auth.password_validation.validate_password`, and
     sets the user's password using
     :meth:`~django.contrib.auth.models.User.set_password()`.

+ 110 - 30
tests/auth_tests/test_forms.py

@@ -1,7 +1,9 @@
 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,
@@ -11,7 +13,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
+from django.core import mail, signals
 from django.core.mail import EmailMultiAlternatives
 from django.forms.fields import CharField, Field, IntegerField
 from django.test import SimpleTestCase, TestCase, override_settings
@@ -27,6 +29,24 @@ 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
@@ -37,9 +57,10 @@ 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(TestDataMixin, TestCase):
+class UserCreationFormTest(ReloadFormsMixin, TestDataMixin, TestCase):
 
     def test_user_already_exists(self):
         data = {
@@ -175,19 +196,25 @@ class UserCreationFormTest(TestDataMixin, TestCase):
         )
 
     def test_custom_form(self):
-        class CustomUserCreationForm(UserCreationForm):
-            class Meta(UserCreationForm.Meta):
-                model = ExtensionUser
-                fields = UserCreationForm.Meta.fields + ('date_of_birth',)
+        with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'):
+            from django.contrib.auth.forms import UserCreationForm
+            self.assertEqual(UserCreationForm.Meta.model, ExtensionUser)
 
-        data = {
-            'username': 'testclient',
-            'password1': 'testclient',
-            'password2': 'testclient',
-            'date_of_birth': '1988-02-24',
-        }
-        form = CustomUserCreationForm(data)
-        self.assertTrue(form.is_valid())
+            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)
 
     def test_custom_form_with_different_username_field(self):
         class CustomUserCreationForm(UserCreationForm):
@@ -261,6 +288,30 @@ class UserCreationFormTest(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())
+
 
 class AuthenticationFormTest(TestDataMixin, TestCase):
 
@@ -605,7 +656,7 @@ class PasswordChangeFormTest(TestDataMixin, TestCase):
         self.assertEqual(form.cleaned_data['new_password2'], data['new_password2'])
 
 
-class UserChangeFormTest(TestDataMixin, TestCase):
+class UserChangeFormTest(ReloadFormsMixin, TestDataMixin, TestCase):
 
     def test_username_validity(self):
         user = User.objects.get(username='testclient')
@@ -679,22 +730,51 @@ class UserChangeFormTest(TestDataMixin, TestCase):
         self.assertEqual(form.initial['password'], form['password'].value())
 
     def test_custom_form(self):
-        class CustomUserChangeForm(UserChangeForm):
-            class Meta(UserChangeForm.Meta):
-                model = ExtensionUser
-                fields = ('username', 'password', 'date_of_birth',)
+        with override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser'):
+            from django.contrib.auth.forms import UserChangeForm
+            self.assertEqual(UserChangeForm.Meta.model, ExtensionUser)
 
-        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))
+            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())
 
 
 @override_settings(TEMPLATES=AUTH_TEMPLATES)