Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #20832 -- Enabled HTML password reset email

Added optional html_email_template_name parameter to password_reset view
and PasswordResetForm.
  • Loading branch information...
commit 6d88d47be6d37234aab86d0e863e371f28347d12 1 parent 94d7fed
@jmichalicek jmichalicek authored timgraham committed
View
1  AUTHORS
@@ -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
9 django/contrib/auth/forms.py
@@ -230,7 +230,7 @@ def save(self, domain_override=None,
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 @@ def save(self, domain_override=None,
# 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
1  django/contrib/auth/tests/templates/registration/html_password_reset_email.html
@@ -0,0 +1 @@
+<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>
View
55 django/contrib/auth/tests/test_forms.py
@@ -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 @@ def test_unusable_password(self):
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
19 django/contrib/auth/tests/test_views.py
@@ -128,6 +128,25 @@ def test_email_found(self):
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
1  django/contrib/auth/tests/urls.py
@@ -67,6 +67,7 @@ def custom_request_auth_login(request):
(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
4 django/contrib/auth/views.py
@@ -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
4 docs/releases/1.7.txt
@@ -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
10 docs/topics/auth/default.txt
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.