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:
Justin Michalicek 2013-07-30 22:29:34 -04:00 committed by Tim Graham
parent 94d7fed775
commit 6d88d47be6
9 changed files with 100 additions and 4 deletions

View File

@ -417,6 +417,7 @@ answer newbie questions, and generally made Django that much better:
Zain Memon
Christian Metts
michal@plovarna.cz
Justin Michalicek <jmichalicek@gmail.com>
Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com>

View File

@ -230,7 +230,7 @@ class PasswordResetForm(forms.Form):
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
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
user.
@ -263,7 +263,12 @@ class PasswordResetForm(forms.Form):
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
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):

View File

@ -0,0 +1 @@
<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
import os
import re
from django import forms
from django.contrib.auth import get_user_model
@ -452,6 +453,60 @@ class PasswordResetFormTest(TestCase):
form.save()
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):

View File

@ -128,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertTrue("http://" in mail.outbox[0].body)
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):
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."

View File

@ -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/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/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})/$',
'django.contrib.auth.views.password_reset_confirm',
dict(post_reset_redirect='/custom/')),

View File

@ -140,7 +140,8 @@ def password_reset(request, is_admin_site=False,
post_reset_redirect=None,
from_email=None,
current_app=None,
extra_context=None):
extra_context=None,
html_email_template_name=None):
if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_done')
else:
@ -155,6 +156,7 @@ def password_reset(request, is_admin_site=False,
'email_template_name': email_template_name,
'subject_template_name': subject_template_name,
'request': request,
'html_email_template_name': html_email_template_name,
}
if is_admin_site:
opts = dict(opts, domain_override=request.get_host())

View File

@ -118,6 +118,10 @@ Minor features
customize the value of :attr:`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
=====================================

View File

@ -793,7 +793,7 @@ patterns.
* ``extra_context``: A dictionary of context data that will be added to the
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
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
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:**
* ``form``: The form (see ``password_reset_form`` above) for resetting