mirror of https://github.com/django/django.git
Fixed #20705 -- Allowed using PasswordResetForm with user models with an email field not named 'email'.
This commit is contained in:
parent
f7e91cac68
commit
617e36dc1e
|
@ -137,6 +137,13 @@ class AbstractBaseUser(models.Model):
|
||||||
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
|
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
|
||||||
return salted_hmac(key_salt, self.password).hexdigest()
|
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
|
@classmethod
|
||||||
def normalize_username(cls, username):
|
def normalize_username(cls, username):
|
||||||
return unicodedata.normalize('NFKC', force_text(username))
|
return unicodedata.normalize('NFKC', force_text(username))
|
||||||
|
|
|
@ -254,8 +254,11 @@ class PasswordResetForm(forms.Form):
|
||||||
that prevent inactive users and users with unusable passwords from
|
that prevent inactive users and users with unusable passwords from
|
||||||
resetting their password.
|
resetting their password.
|
||||||
"""
|
"""
|
||||||
active_users = get_user_model()._default_manager.filter(
|
UserModel = get_user_model()
|
||||||
email__iexact=email, is_active=True)
|
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())
|
return (u for u in active_users if u.has_usable_password())
|
||||||
|
|
||||||
def save(self, domain_override=None,
|
def save(self, domain_override=None,
|
||||||
|
@ -277,7 +280,7 @@ class PasswordResetForm(forms.Form):
|
||||||
else:
|
else:
|
||||||
site_name = domain = domain_override
|
site_name = domain = domain_override
|
||||||
context = {
|
context = {
|
||||||
'email': user.email,
|
'email': email,
|
||||||
'domain': domain,
|
'domain': domain,
|
||||||
'site_name': site_name,
|
'site_name': site_name,
|
||||||
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
|
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
|
||||||
|
@ -289,7 +292,7 @@ class PasswordResetForm(forms.Form):
|
||||||
context.update(extra_email_context)
|
context.update(extra_email_context)
|
||||||
self.send_mail(
|
self.send_mail(
|
||||||
subject_template_name, email_template_name, context, from_email,
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -333,6 +333,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
|
EMAIL_FIELD = 'email'
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
REQUIRED_FIELDS = ['email']
|
REQUIRED_FIELDS = ['email']
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,11 @@ Minor features
|
||||||
* The :func:`~django.contrib.auth.signals.user_login_failed` signal now
|
* The :func:`~django.contrib.auth.signals.user_login_failed` signal now
|
||||||
receives a ``request`` argument.
|
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`
|
:mod:`django.contrib.contenttypes`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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
|
value (the :attr:`~django.db.models.Field.primary_key` by default) of an
|
||||||
existing instance.
|
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
|
.. attribute:: REQUIRED_FIELDS
|
||||||
|
|
||||||
A list of the field names that will be prompted for when creating a
|
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
|
override this method, be sure to call ``super()`` to retain the
|
||||||
normalization.
|
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)
|
.. classmethod:: normalize_username(username)
|
||||||
|
|
||||||
.. versionadded:: 1.10
|
.. 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:
|
if those assumptions are met:
|
||||||
|
|
||||||
* :class:`~django.contrib.auth.forms.PasswordResetForm`: Assumes that the user
|
* :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
|
model has a field that stores the user's email address with the name returned
|
||||||
boolean field named ``is_active`` to prevent password resets for inactive
|
by :meth:`~models.AbstractBaseUser.get_email_field_name` (``email`` by
|
||||||
users.
|
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
|
Finally, the following forms are tied to
|
||||||
:class:`~django.contrib.auth.models.User` and need to be rewritten or extended
|
:class:`~django.contrib.auth.models.User` and need to be rewritten or extended
|
||||||
|
|
|
@ -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()
|
|
@ -26,6 +26,7 @@ from django.utils.translation import ugettext as _
|
||||||
from .models.custom_user import (
|
from .models.custom_user import (
|
||||||
CustomUser, CustomUserWithoutIsActiveField, ExtensionUser,
|
CustomUser, CustomUserWithoutIsActiveField, ExtensionUser,
|
||||||
)
|
)
|
||||||
|
from .models.with_custom_email_field import CustomEmailField
|
||||||
from .models.with_integer_username import IntegerUsernameUser
|
from .models.with_integer_username import IntegerUsernameUser
|
||||||
from .settings import AUTH_TEMPLATES
|
from .settings import AUTH_TEMPLATES
|
||||||
|
|
||||||
|
@ -812,6 +813,17 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
|
||||||
message.get_payload(1).get_payload()
|
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):
|
class ReadOnlyPasswordHashTest(SimpleTestCase):
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.global_settings import PASSWORD_HASHERS
|
from django.conf.global_settings import PASSWORD_HASHERS
|
||||||
from django.contrib.auth import get_user_model
|
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.hashers import get_hasher
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractUser, Group, Permission, User, UserManager,
|
AbstractUser, Group, Permission, User, UserManager,
|
||||||
|
@ -12,6 +13,8 @@ from django.core import mail
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.test import TestCase, mock, override_settings
|
from django.test import TestCase, mock, override_settings
|
||||||
|
|
||||||
|
from .models.with_custom_email_field import CustomEmailField
|
||||||
|
|
||||||
|
|
||||||
class NaturalKeysTestCase(TestCase):
|
class NaturalKeysTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -160,6 +163,14 @@ class AbstractBaseUserTests(TestCase):
|
||||||
self.assertNotEqual(username, ohm_username)
|
self.assertNotEqual(username, ohm_username)
|
||||||
self.assertEqual(username, 'iamtheΩ') # U+03A9 GREEK CAPITAL LETTER OMEGA
|
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):
|
class AbstractUserTestCase(TestCase):
|
||||||
def test_email_user(self):
|
def test_email_user(self):
|
||||||
|
|
Loading…
Reference in New Issue