How To¶
Here we will explore some examples using class-based emails.
Sending a Welcome Email¶
In the introduction to class-based emails, we demonstrated some very basic patterns for sending a welcome email. Here, we will expand on those patterns a bit further to demonstrate a real use case example.
Our goal here will be to send a welcome email to newly registered users by
hooking into the post_save
signal provided by Django.
Password Reset Email¶
Sending a password reset email manually. First lets take a look at how Django
does this in the built in PasswordResetForm
packaged with
django.contrib.auth
.:
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
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):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
from django.core.mail import send_mail
UserModel = get_user_model()
email = self.cleaned_data["email"]
users = UserModel._default_manager.filter(email__iexact=email)
for user in users:
# Make sure that no email is sent to a user that actually has
# a password marked as unusable
if user.password == UNUSABLE_PASSWORD:
continue
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override
c = {
'email': user.email,
'domain': domain,
'site_name': site_name,
'uid': int_to_base36(user.pk),
'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http',
}
subject = loader.render_to_string(subject_template_name, c)
# 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])
Given a valid email address, the password reset form does the following.
- Lookup all users with that email address, skipping users who have unusable passwords.
- Figure out the domain, site information, and other context data.
- Render the password reset template.
- Send the password reset email(s).
Our goal will be to reproduce this logic while leveraging the power of class-based views.:
# accounts/emails.py
class PasswordResetEmail(HTMLEmail):
...
send_password_reset_email = PasswordResetEmail.as_callable()
And to use it from a view.
# accounts/views.py
from accounts.emails import send_password_reset_email
class PasswordResetView(FormView):
...
def form_valid(self, form):
# Send the password reset email.
email = form.cleaned_data['email']
users = UserModel._default_manager.filter(email__iexact=email)
for user in users:
password_reset_email(user)
return super(PasswordResetView, self).form_valid(form)
Now that we know what our interface should look like, lets start writing our email class.
Step 1: Writing the basic view¶
First, we need a way to find all of the users who’s email matches our target email. Since we need to send a password reset email for every user with the target email, this logic needs to live outside of our email class. For this example, i’ll simply make a function to wrap around our email callable.
# accounts/emails.py
from django.contrib.auth import get_user_model, UNUSABLE_PASSWORD
from emailtools import HTMLEmail
UserModel = get_user_model()
class PasswordResetEmail(HTMLEmail):
from_address = 'admin@example.com'
subject = 'Password reset on example.com'
template_name = 'registration/password_reset_email.html'
def get_to(self):
return [self.args[0].email]
send_password_reset_email = PasswordResetEmail.as_callable()
Step 2: Domain and Site information.¶
Now lets get our site and domain information, along with the other context
information ready for template rendering. For this, we’ll want to hook into
the method call to get_context_data()
.:
# accounts/emails.py
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from emailtools import HTMLEmail
class PasswordResetEmail(HTMLEmail):
token_generator = default_token_generator
...
def get_context_data(self, **kwargs):
kwargs = super(PasswordResetEmail, self).get_context_data(**kwargs)
current_site = Site.objects.get_current()
kwargs.update({
'site_name': current_site.name,
'domain': current_site.domain,
'uid': int_to_base36(user.pk),
'email': self.args[0].email,
'user': self.args[0],
'token': self.token_generator.make_token(user),
})
return kwargs
While this will suffice for reproducing the behavior of
save()
, constructing urls in
templates via string concatenation has always seemed prone to human error.
Additionally, there are so many uses for email tokens so wouldn’t it be nice to
have a reusable tool for sending such emails.
Step 3: Refactoring out the Re-usable components¶
First, lets write BuildAbsoluteURIMixin
, a mixin class for your email classes
which provides the url reversing that returns absolute urls.
# mixins.py
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
class BuildAbsoluteURIMixin(object):
protocol = 'http'
def get_domain(self):
return Site.objects.get_current().domain
def get_protocol(self):
return self.protocol
def reverse_absolute_uri(self, view_name, args=None, kwargs=None):
location = reverse(view_name, args=args, kwargs=kwargs)
return self.build_absolute_uri(location)
def build_absolute_uri(self, location):
return '{protocol}://{domain}{location}'.format(
protocol=self.get_protocol(),
domain=self.get_domain(),
location=location,
)
Now, lets write a UserTokenEmailMixin
which will provide user based
token generation for our emails.
# mixins.py
from django.utils.http import int_to_base36
class UserTokenEmailMixin(BuildAbsoluteURIMixin):
UID_KWARG = 'uidb36'
TOKEN_KWARG = 'token'
token_generator = default_token_generator
def get_user(self):
return self.args[0]
def generate_token(self, user):
return self.token_generator.make_token(user)
def get_uid(self, user):
return int_to_base36(user.pk)
def reverse_token_url(self, view_name, args=None, kwargs={}):
kwargs.setdefault(self.UID_KWARG, self.get_uid(self.get_user()))
kwargs.setdefault(self.TOKEN_KWARG, self.generate_token(self.get_user()))
return self.reverse_absolute_uri(view_name, args=args, kwargs=kwargs)
Step 4: Bringing it all together¶
Now, lets rewrite PasswordResetEmail
to make use of these new mixins.
# accounts/emails.py
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from emailtools import HTMLEmail
from mixins import UserTokenEmailMixin
class PasswordResetEmail(UserTokenEmailMixin, MarkdownEmail):
from_email = 'admin@example.com'
template_name = 'registration/password_reset_email.html'
subject = "Password Reset"
def get_to(self):
return [self.get_user().email]
def get_context_data(self, **kwargs):
kwargs = super(PasswordResetEmail, self).get_context_data()
user = self.get_user()
kwargs.update({
'user': user,
'reset_url': self.reverse_token_url('password_reset_confirm'),
})
return kwargs
send_password_reset_email = PasswordResetEmail.as_callable()
Step 5: Re-usability¶
A simple pattern for requiring email verification is to remove the password fields from the signup form and send an email verification link on account creation. This has the pleasant side effect of simplifying the signup process while verifying your user’s email addresses.
Class based emails really shine here. Lets look at what it would take to use
our PasswordResetEmail
class to send a welcome email.
# accounts/emails.py
send_welcome_email = PasswordResetEmail.as_callable(
subject='Welcome to example.com'
template_name='registration/welcome_email.html',
)
The two mixins found in this example are also available in email tools.