Sending Automated Emails with Django Signals
Django is an open-source python based web framework. It is designed to allow for rapid development of web applications by including much of the expected functionality...
Django allows for secure rapid development of web applications
Django is an open-source python based web framework. It is designed to allow for rapid development of web applications by including much of the expected functionality right out of the box.
One example of this functionality is the “signal dispatcher” which facilitates communication between disparate pieces of code in a Django project. As the blog post title indicates, we are going to use one of these signals to trigger Django’s send_mail() function. Django uses Python’s smtplib
module to send mail but provides some wrappers on it to expand its functionality and make testing easier.
Many of the built-in signals in Django are associated with database models. There are signals related to the stages of the save() and delete() methods, e.g. django.db.models.signals.pre_save, django.db.models.signals.post_save, django.db.models.signals.pre_delete & django.db.models.signals.post_delete.
There are also signals that relate to HTTP requests, but for the purposes of this blog post, I am going to focus on models actions.
These signals pair naturally with behavior like sending an email. For example, we recently implemented In-App feedback/help functionality for one of our clients on their Django-based web app. They requested that any time new feedback or a new help request was submitted, their customer service team would be notified via email. Since the feedback/help request instance was stored in the database we settled on the post_save
signal to trigger the email.
Let’s recreate a case similar to the one I described above. I am going to re-use the models I created for my previous blog post on Django admin inlines. Here is the models.py file for the Starfleet app:
from django.db import models
# Create your models here.
class Officer(models.Model):
name = models.CharField(max_length=256)
rank = models.ForeignKey('Rank', on_delete=models.CASCADE)
ship_assignment = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return self.name
class Rank(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return self.name
class Ship(models.Model):
name = models.CharField(max_length=256)
registry = models.CharField(max_length=256)
captain = models.OneToOneField('Officer', default=None, on_delete=models.CASCADE, blank=True, null=True)
ship_class = models.CharField(max_length=256)
status = models.CharField(max_length=256)
def __str__(self):
return self.name
Now let’s assume we want to send an email whenever a new officer is added to the Starfleet app. In order to implement this we are going to have to add some import statements to the top of the file:
from django.core.mail import send_mail
from django.db.models.signals import post_save
from django.dispatch import receiver
As I stated above, send_mail
uses Python’s smtplib
and unsurprisingly we’ll use it to send the email. We will use the post_save
signal to trigger the event when the new officer model is saved. And we will use a receiver
function to do the actual processing to call send_mail
.
In order to connect the post_save
signal to our receiver function, we are going to use a receiver() decorator
. Putting it all together, the function looks like this:
@receiver(post_save, sender=Officer)
def send_new_officer_notification_email(sender, instance, created, **kwargs):
# if a new officer is created, compose and send the email
if created:
name = instance.name if instance.name else "no name given"
rank = instance.rank.name if instance.rank else "no rank given"
ship_assignment = instance.ship_assignment.name if instance.ship_assignment else 'no ship assignment'
subject = 'NAME: {0}, RANK: {1}, SHIP ASSIGNMENT: {2}'.format(name, rank, ship_assignment)
message = 'A New Officer has been assigned!\n'
message += 'NAME: ' + name + '\n' + 'RANK: ' \
+ rank + '\n' + 'SHIP ASSIGNMENT: ' + ship_assignment + '\n'
message += '--' * 30
send_mail(
subject,
message,
'your_email@example.com',
['recipeint1@xample.com', 'recipent2@xample.com '],
fail_silently=False,
)
Here’s a quick overview of the arguments for the receiver function send_new_officer_notification_email()
sender
refers to the model class.instance
refers to the actual instance of the model that is being saved. We’re going to use its attributes to populate the email with relevant info.created
is a boolean value that is True if a new record is created (as opposed to an existing record being updated)
And as for **kwargs
, this is a “wildcard” argument that can contain a dictionary of keywords. It’s not relevant to this case but if you leave this argument out, Django will throw an error.
If the save() call results in a new record being created, we’ll generate the message using python string formatting and the attributes of the officer class, “name, rank, ship_assignment”.
Here’s an overview of the arguments for the send_mail()
function:
subject
- the email subjectmessage
- the email body- 'your_email@example.com' - the email from address
- ['recipeint1@example.com', 'recipeint2@example.com'] - the email to addresses
fail_silently
is a boolean. If it is false, send_mail() will raise ansmtplib.SMTPException
if there is an error.
This gives us a modified models.py
file that looks like this:
from django.db import models
from django.core.mail import send_mail
from django.db.models.signals import post_save
from django.dispatch import receiver
# Create your models here.
class Officer(models.Model):
name = models.CharField(max_length=256)
rank = models.ForeignKey('Rank', on_delete=models.CASCADE)
ship_assignment = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return self.name
class Rank(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return self.name
class Ship(models.Model):
name = models.CharField(max_length=256)
registry = models.CharField(max_length=256)
captain = models.OneToOneField('Officer', default=None, on_delete=models.CASCADE, blank=True, null=True)
ship_class = models.CharField(max_length=256)
status = models.CharField(max_length=256)
def __str__(self):
return self.name
@receiver(post_save, sender=Officer)
def send_new_officer_notification_email(sender, instance, created, **kwargs):
# if a new officer is created, compose and send the email
if created:
name = instance.name if instance.name else "no name given"
rank = instance.rank.name if instance.rank else "no rank given"
ship_assignment = instance.ship_assignment.name if instance.ship_assignment else 'no ship assignment'
subject = 'NAME: {0}, RANK: {1}, SHIP ASSIGNMENT: {2}'.format(name, rank, ship_assignment)
message = 'A New Officer has been assigned!\n'
message += 'NAME: ' + name + '\n' + 'RANK: ' \
+ rank + '\n' + 'SHIP ASSIGNMENT: ' + ship_assignment + '\n'
message += '--' * 30
send_mail(
subject,
message,
'your_email@example.com',
['recipeint1@xample.com', 'recipent2@xample.com '],
fail_silently=False,
)
The last step we need to take is to add some values to the root settings.py
file. The values you assign to these settings are going to vary depending on your system’s configuration and your email host’s configurations.
MAIL_HOST = 'email-smtp.your.email.host.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = 'your.host.username'
EMAIL_HOST_PASSWORD = 'aReally$trongP4ssword'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
MAILER_EMAIL_BACKEND = EMAIL_BACKEND
In this case, I have set EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
for testing purposes. This causes the email to be output to the Django console which is extremely handy.
Now I want to go into the admin console and create a new officer in order to trigger the email:
Verify that the record was saved:
And we can expect to see the formatted email as output in the console:
To clarify the above screenshot, this is the output:
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: NAME: Beverly Crusher, M.D., RANK: Commander,
SHIP ASSIGNMENT: USS Enterprise
From: your_email@example.com
To: recipeint1@xample.com, recipent2@xample.com
Date: Mon, 22 Feb 2021 22:18:32 -0000
Message-ID: <161403231277.13758.10023986492893831515@bens-mbp.local>
A New Officer has been assigned!
NAME: Beverly Crusher, M.D.
RANK: Commander
SHIP ASSIGNMENT: USS Enterprise
------------------------------------------------------------
-------------------------------------------------------------------------------
Django is free and open-source and allows developers to take their applications from idea to completion quickly. Feel free to contact us for a free consultation if assistance is needed with your project.
The JBS Quick Launch Lab
Free Qualified Assessment
Quantify what it will take to implement your next big idea!
Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.