From 617e36dc1ede2a311dfd03f18432b31cbfe4c0f7 Mon Sep 17 00:00:00 2001 From: levental Date: Mon, 19 Sep 2016 14:55:18 +0200 Subject: [PATCH] Fixed #20705 -- Allowed using PasswordResetForm with user models with an email field not named 'email'. --- django/contrib/auth/base_user.py | 7 ++++++ django/contrib/auth/forms.py | 11 +++++---- django/contrib/auth/models.py | 1 + docs/releases/1.11.txt | 5 ++++ docs/topics/auth/customizing.txt | 23 ++++++++++++++++--- .../models/with_custom_email_field.py | 23 +++++++++++++++++++ tests/auth_tests/test_forms.py | 12 ++++++++++ tests/auth_tests/test_models.py | 11 +++++++++ 8 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/auth_tests/models/with_custom_email_field.py diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py index de069659e1..9ad5cde87f 100644 --- a/django/contrib/auth/base_user.py +++ b/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)) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 7a6a8d74aa..dbdc08db64 100644 --- a/django/contrib/auth/forms.py +++ b/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, ) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index e62307a0a7..eef38c17ee 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -333,6 +333,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin): objects = UserManager() + EMAIL_FIELD = 'email' USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index ded12408b9..411c6fb3ab 100644 --- a/docs/releases/1.11.txt +++ b/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 + ` to the name of the field. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 068671c259..f3b53f17f5 100644 --- a/docs/topics/auth/customizing.txt +++ b/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 diff --git a/tests/auth_tests/models/with_custom_email_field.py b/tests/auth_tests/models/with_custom_email_field.py new file mode 100644 index 0000000000..a98b02b8f1 --- /dev/null +++ b/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() diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index c0e2961424..8d656bd6f9 100644 --- a/tests/auth_tests/test_forms.py +++ b/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): diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py index a92f882de0..c939f3437c 100644 --- a/tests/auth_tests/test_models.py +++ b/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):