Browse Source

Fixed #20705 -- Allowed using PasswordResetForm with user models with an email field not named 'email'.

levental 8 years ago
parent
commit
617e36dc1e

+ 7 - 0
django/contrib/auth/base_user.py

@@ -137,6 +137,13 @@ class AbstractBaseUser(models.Model):
         key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
         return salted_hmac(key_salt, self.password).hexdigest()
 
+    @classmethod
+    def get_email_field_name(cls):
+        try:
+            return cls.EMAIL_FIELD
+        except AttributeError:
+            return 'email'
+
     @classmethod
     def normalize_username(cls, username):
         return unicodedata.normalize('NFKC', force_text(username))

+ 7 - 4
django/contrib/auth/forms.py

@@ -254,8 +254,11 @@ class PasswordResetForm(forms.Form):
         that prevent inactive users and users with unusable passwords from
         resetting their password.
         """
-        active_users = get_user_model()._default_manager.filter(
-            email__iexact=email, is_active=True)
+        UserModel = get_user_model()
+        active_users = UserModel._default_manager.filter(**{
+            '%s__iexact' % UserModel.get_email_field_name(): email,
+            'is_active': True,
+        })
         return (u for u in active_users if u.has_usable_password())
 
     def save(self, domain_override=None,
@@ -277,7 +280,7 @@ class PasswordResetForm(forms.Form):
             else:
                 site_name = domain = domain_override
             context = {
-                'email': user.email,
+                'email': email,
                 'domain': domain,
                 'site_name': site_name,
                 'uid': urlsafe_base64_encode(force_bytes(user.pk)),
@@ -289,7 +292,7 @@ class PasswordResetForm(forms.Form):
                 context.update(extra_email_context)
             self.send_mail(
                 subject_template_name, email_template_name, context, from_email,
-                user.email, html_email_template_name=html_email_template_name,
+                email, html_email_template_name=html_email_template_name,
             )
 
 

+ 1 - 0
django/contrib/auth/models.py

@@ -333,6 +333,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
 
     objects = UserManager()
 
+    EMAIL_FIELD = 'email'
     USERNAME_FIELD = 'username'
     REQUIRED_FIELDS = ['email']
 

+ 5 - 0
docs/releases/1.11.txt

@@ -120,6 +120,11 @@ Minor features
 * The :func:`~django.contrib.auth.signals.user_login_failed` signal now
   receives a ``request`` argument.
 
+* :class:`~django.contrib.auth.forms.PasswordResetForm` supports custom user
+  models that use an email field named something other than ``'email'``.
+  Set :attr:`CustomUser.EMAIL_FIELD
+  <django.contrib.auth.models.CustomUser.EMAIL_FIELD>` to the name of the field.
+
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 20 - 3
docs/topics/auth/customizing.txt

@@ -544,6 +544,14 @@ password resets. You must then provide some key implementation details:
         value (the :attr:`~django.db.models.Field.primary_key` by default) of an
         existing instance.
 
+    .. attribute:: EMAIL_FIELD
+
+        .. versionadded:: 1.11
+
+        A string describing the name of the email field on the ``User`` model.
+        This value is returned by
+        :meth:`~models.AbstractBaseUser.get_email_field_name`.
+
     .. attribute:: REQUIRED_FIELDS
 
         A list of the field names that will be prompted for when creating a
@@ -623,6 +631,14 @@ The following attributes and methods are available on any subclass of
         override this method, be sure to call ``super()`` to retain the
         normalization.
 
+    .. classmethod:: get_email_field_name()
+
+       .. versionadded:: 1.11
+
+       Returns the name of the email field specified by the
+       :attr:`~models.CustomUser.EMAIL_FIELD` attribute. Defaults to
+       ``'email'`` if ``EMAIL_FIELD`` isn't specified.
+
     .. classmethod:: normalize_username(username)
 
         .. versionadded:: 1.10
@@ -807,9 +823,10 @@ The following forms make assumptions about the user model and can be used as-is
 if those assumptions are met:
 
 * :class:`~django.contrib.auth.forms.PasswordResetForm`: Assumes that the user
-  model has a field named ``email`` that can be used to identify the user and a
-  boolean field named ``is_active`` to prevent password resets for inactive
-  users.
+  model has a field that stores the user's email address with the name returned
+  by :meth:`~models.AbstractBaseUser.get_email_field_name` (``email`` by
+  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

+ 23 - 0
tests/auth_tests/models/with_custom_email_field.py

@@ -0,0 +1,23 @@
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.contrib.auth.models import BaseUserManager
+from django.db import models
+
+
+class CustomEmailFieldUserManager(BaseUserManager):
+    def create_user(self, username, password, email):
+        user = self.model(username=username)
+        user.set_password(password)
+        user.email_address = email
+        user.save(using=self._db)
+        return user
+
+
+class CustomEmailField(AbstractBaseUser):
+    username = models.CharField(max_length=255)
+    password = models.CharField(max_length=255)
+    email_address = models.EmailField()
+    is_active = models.BooleanField(default=True)
+
+    EMAIL_FIELD = 'email_address'
+
+    objects = CustomEmailFieldUserManager()

+ 12 - 0
tests/auth_tests/test_forms.py

@@ -26,6 +26,7 @@ from django.utils.translation import ugettext as _
 from .models.custom_user import (
     CustomUser, CustomUserWithoutIsActiveField, ExtensionUser,
 )
+from .models.with_custom_email_field import CustomEmailField
 from .models.with_integer_username import IntegerUsernameUser
 from .settings import AUTH_TEMPLATES
 
@@ -812,6 +813,17 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
             message.get_payload(1).get_payload()
         ))
 
+    @override_settings(AUTH_USER_MODEL='auth_tests.CustomEmailField')
+    def test_custom_email_field(self):
+        email = 'test@mail.com'
+        CustomEmailField.objects.create_user('test name', 'test password', email)
+        form = PasswordResetForm({'email': email})
+        self.assertTrue(form.is_valid())
+        form.save()
+        self.assertEqual(form.cleaned_data['email'], email)
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].to, [email])
+
 
 class ReadOnlyPasswordHashTest(SimpleTestCase):
 

+ 11 - 0
tests/auth_tests/test_models.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 
 from django.conf.global_settings import PASSWORD_HASHERS
 from django.contrib.auth import get_user_model
+from django.contrib.auth.base_user import AbstractBaseUser
 from django.contrib.auth.hashers import get_hasher
 from django.contrib.auth.models import (
     AbstractUser, Group, Permission, User, UserManager,
@@ -12,6 +13,8 @@ from django.core import mail
 from django.db.models.signals import post_save
 from django.test import TestCase, mock, override_settings
 
+from .models.with_custom_email_field import CustomEmailField
+
 
 class NaturalKeysTestCase(TestCase):
 
@@ -160,6 +163,14 @@ class AbstractBaseUserTests(TestCase):
                 self.assertNotEqual(username, ohm_username)
                 self.assertEqual(username, 'iamtheΩ')  # U+03A9 GREEK CAPITAL LETTER OMEGA
 
+    def test_default_email(self):
+        user = AbstractBaseUser()
+        self.assertEqual(user.get_email_field_name(), 'email')
+
+    def test_custom_email(self):
+        user = CustomEmailField()
+        self.assertEqual(user.get_email_field_name(), 'email_address')
+
 
 class AbstractUserTestCase(TestCase):
     def test_email_user(self):