Fixed #20832 -- Enabled HTML password reset email
Added optional html_email_template_name parameter to password_reset view and PasswordResetForm.
This commit is contained in:
parent
94d7fed775
commit
6d88d47be6
1
AUTHORS
1
AUTHORS
|
@ -417,6 +417,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Zain Memon
|
Zain Memon
|
||||||
Christian Metts
|
Christian Metts
|
||||||
michal@plovarna.cz
|
michal@plovarna.cz
|
||||||
|
Justin Michalicek <jmichalicek@gmail.com>
|
||||||
Slawek Mikula <slawek dot mikula at gmail dot com>
|
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||||
Katie Miller <katie@sub50.com>
|
Katie Miller <katie@sub50.com>
|
||||||
Shawn Milochik <shawn@milochik.com>
|
Shawn Milochik <shawn@milochik.com>
|
||||||
|
|
|
@ -230,7 +230,7 @@ class PasswordResetForm(forms.Form):
|
||||||
subject_template_name='registration/password_reset_subject.txt',
|
subject_template_name='registration/password_reset_subject.txt',
|
||||||
email_template_name='registration/password_reset_email.html',
|
email_template_name='registration/password_reset_email.html',
|
||||||
use_https=False, token_generator=default_token_generator,
|
use_https=False, token_generator=default_token_generator,
|
||||||
from_email=None, request=None):
|
from_email=None, request=None, html_email_template_name=None):
|
||||||
"""
|
"""
|
||||||
Generates a one-use only link for resetting password and sends to the
|
Generates a one-use only link for resetting password and sends to the
|
||||||
user.
|
user.
|
||||||
|
@ -263,7 +263,12 @@ class PasswordResetForm(forms.Form):
|
||||||
# Email subject *must not* contain newlines
|
# Email subject *must not* contain newlines
|
||||||
subject = ''.join(subject.splitlines())
|
subject = ''.join(subject.splitlines())
|
||||||
email = loader.render_to_string(email_template_name, c)
|
email = loader.render_to_string(email_template_name, c)
|
||||||
send_mail(subject, email, from_email, [user.email])
|
|
||||||
|
if html_email_template_name:
|
||||||
|
html_email = loader.render_to_string(html_email_template_name, c)
|
||||||
|
else:
|
||||||
|
html_email = None
|
||||||
|
send_mail(subject, email, from_email, [user.email], html_message=html_email)
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordForm(forms.Form):
|
class SetPasswordForm(forms.Form):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -452,6 +453,60 @@ class PasswordResetFormTest(TestCase):
|
||||||
form.save()
|
form.save()
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
|
||||||
|
TEMPLATE_DIRS=(
|
||||||
|
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_save_plaintext_email(self):
|
||||||
|
"""
|
||||||
|
Test the PasswordResetForm.save() method with no html_email_template_name
|
||||||
|
parameter passed in.
|
||||||
|
Test to ensure original behavior is unchanged after the parameter was added.
|
||||||
|
"""
|
||||||
|
(user, username, email) = self.create_dummy_user()
|
||||||
|
form = PasswordResetForm({"email": email})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
form.save()
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
message = mail.outbox[0].message()
|
||||||
|
self.assertFalse(message.is_multipart())
|
||||||
|
self.assertEqual(message.get_content_type(), 'text/plain')
|
||||||
|
self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
|
||||||
|
self.assertEqual(len(mail.outbox[0].alternatives), 0)
|
||||||
|
self.assertEqual(message.get_all('to'), [email])
|
||||||
|
self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload()))
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
|
||||||
|
TEMPLATE_DIRS=(
|
||||||
|
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_save_html_email_template_name(self):
|
||||||
|
"""
|
||||||
|
Test the PasswordResetFOrm.save() method with html_email_template_name
|
||||||
|
parameter specified.
|
||||||
|
Test to ensure that a multipart email is sent with both text/plain
|
||||||
|
and text/html parts.
|
||||||
|
"""
|
||||||
|
(user, username, email) = self.create_dummy_user()
|
||||||
|
form = PasswordResetForm({"email": email})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
form.save(html_email_template_name='registration/html_password_reset_email.html')
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(len(mail.outbox[0].alternatives), 1)
|
||||||
|
message = mail.outbox[0].message()
|
||||||
|
self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
|
||||||
|
self.assertEqual(len(message.get_payload()), 2)
|
||||||
|
self.assertTrue(message.is_multipart())
|
||||||
|
self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
|
||||||
|
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
|
||||||
|
self.assertEqual(message.get_all('to'), [email])
|
||||||
|
self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload()))
|
||||||
|
self.assertTrue(re.match(r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$', message.get_payload(1).get_payload()))
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyPasswordHashTest(TestCase):
|
class ReadOnlyPasswordHashTest(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase):
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
self.assertTrue("http://" in mail.outbox[0].body)
|
self.assertTrue("http://" in mail.outbox[0].body)
|
||||||
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
|
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
|
||||||
|
# optional multipart text/html email has been added. Make sure original,
|
||||||
|
# default functionality is 100% the same
|
||||||
|
self.assertFalse(mail.outbox[0].message().is_multipart())
|
||||||
|
|
||||||
|
def test_html_mail_template(self):
|
||||||
|
"""
|
||||||
|
A multipart email with text/plain and text/html is sent
|
||||||
|
if the html_email_template parameter is passed to the view
|
||||||
|
"""
|
||||||
|
response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
message = mail.outbox[0].message()
|
||||||
|
self.assertEqual(len(message.get_payload()), 2)
|
||||||
|
self.assertTrue(message.is_multipart())
|
||||||
|
self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
|
||||||
|
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
|
||||||
|
self.assertTrue('<html>' not in message.get_payload(0).get_payload())
|
||||||
|
self.assertTrue('<html>' in message.get_payload(1).get_payload())
|
||||||
|
|
||||||
def test_email_found_custom_from(self):
|
def test_email_found_custom_from(self):
|
||||||
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
|
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
|
||||||
|
|
|
@ -67,6 +67,7 @@ urlpatterns = urlpatterns + patterns('',
|
||||||
(r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
|
(r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
|
||||||
(r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')),
|
(r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')),
|
||||||
(r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')),
|
(r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')),
|
||||||
|
(r'^password_reset/html_email_template/$', 'django.contrib.auth.views.password_reset', dict(html_email_template_name='registration/html_password_reset_email.html')),
|
||||||
(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||||
'django.contrib.auth.views.password_reset_confirm',
|
'django.contrib.auth.views.password_reset_confirm',
|
||||||
dict(post_reset_redirect='/custom/')),
|
dict(post_reset_redirect='/custom/')),
|
||||||
|
|
|
@ -140,7 +140,8 @@ def password_reset(request, is_admin_site=False,
|
||||||
post_reset_redirect=None,
|
post_reset_redirect=None,
|
||||||
from_email=None,
|
from_email=None,
|
||||||
current_app=None,
|
current_app=None,
|
||||||
extra_context=None):
|
extra_context=None,
|
||||||
|
html_email_template_name=None):
|
||||||
if post_reset_redirect is None:
|
if post_reset_redirect is None:
|
||||||
post_reset_redirect = reverse('password_reset_done')
|
post_reset_redirect = reverse('password_reset_done')
|
||||||
else:
|
else:
|
||||||
|
@ -155,6 +156,7 @@ def password_reset(request, is_admin_site=False,
|
||||||
'email_template_name': email_template_name,
|
'email_template_name': email_template_name,
|
||||||
'subject_template_name': subject_template_name,
|
'subject_template_name': subject_template_name,
|
||||||
'request': request,
|
'request': request,
|
||||||
|
'html_email_template_name': html_email_template_name,
|
||||||
}
|
}
|
||||||
if is_admin_site:
|
if is_admin_site:
|
||||||
opts = dict(opts, domain_override=request.get_host())
|
opts = dict(opts, domain_override=request.get_host())
|
||||||
|
|
|
@ -118,6 +118,10 @@ Minor features
|
||||||
customize the value of :attr:`ModelAdmin.fields
|
customize the value of :attr:`ModelAdmin.fields
|
||||||
<django.contrib.admin.ModelAdmin.fields>`.
|
<django.contrib.admin.ModelAdmin.fields>`.
|
||||||
|
|
||||||
|
* :func:`django.contrib.auth.views.password_reset` takes an optional
|
||||||
|
``html_email_template_name`` parameter used to send a multipart HTML email
|
||||||
|
for password resets.
|
||||||
|
|
||||||
Backwards incompatible changes in 1.7
|
Backwards incompatible changes in 1.7
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -793,7 +793,7 @@ patterns.
|
||||||
* ``extra_context``: A dictionary of context data that will be added to the
|
* ``extra_context``: A dictionary of context data that will be added to the
|
||||||
default context data passed to the template.
|
default context data passed to the template.
|
||||||
|
|
||||||
.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context])
|
.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context, html_email_template_name])
|
||||||
|
|
||||||
Allows a user to reset their password by generating a one-time use link
|
Allows a user to reset their password by generating a one-time use link
|
||||||
that can be used to reset the password, and sending that link to the
|
that can be used to reset the password, and sending that link to the
|
||||||
|
@ -856,6 +856,14 @@ patterns.
|
||||||
* ``extra_context``: A dictionary of context data that will be added to the
|
* ``extra_context``: A dictionary of context data that will be added to the
|
||||||
default context data passed to the template.
|
default context data passed to the template.
|
||||||
|
|
||||||
|
* ``html_email_template_name``: The full name of a template to use
|
||||||
|
for generating a ``text/html`` multipart email with the password reset
|
||||||
|
link. By default, HTML email is not sent.
|
||||||
|
|
||||||
|
.. versionadded:: 1.7
|
||||||
|
|
||||||
|
``html_email_template_name`` was added.
|
||||||
|
|
||||||
**Template context:**
|
**Template context:**
|
||||||
|
|
||||||
* ``form``: The form (see ``password_reset_form`` above) for resetting
|
* ``form``: The form (see ``password_reset_form`` above) for resetting
|
||||||
|
|
Loading…
Reference in New Issue