Sending Emails With Django Templates Stored in a Database

Feb 17, 2018 11:59 · 520 words · 3 minute read python email Django snippet

I want to have an email templates stored in my Django database so that admins can manage them without me changing the code. The requirement is to have HTML-emails support as well as an ability to send files attached. Neither standard Django send_mail nor other builtin features don’t support that feature so below you can find my solution to that problem.

The snippet below relies to the Django’s EmailMultiAlternatives and basically wraps it into a class.

Model definition

The model provides capabilities to store the email template in the database to allow admins to manage these templates easily.

  • subject*- An email subject.
  • template_key* - A unique identifier of the email template that could be referenced from the code.
  • to_email - Optional. A default email that the template will be sent to. Can be used to send email to admins.
  • from_email - Optional. A default email that the template will be sent from. If not specified, settings.DEFAULT_FROM_EMAIL is used.
  • html_template and plain_text- Body of the email in text and html formats.
from django import template
from django.conf import settings
from django.core.mail import send_mail, EmailMultiAlternatives
from django.db import models
from django.template import Context


class EmailTemplate(models.Model):
    """
    Email templates get stored in database so that admins can
    change emails on the fly
    """
    subject = models.CharField(max_length=255, blank=True, null=True)
    to_email = models.CharField(max_length=255, blank=True, null=True)
    from_email = models.CharField(max_length=255, blank=True, null=True)
    html_template = models.TextField(blank=True, null=True)
    plain_text = models.TextField(blank=True, null=True)
    is_html = models.BooleanField(default=False)
    is_text = models.BooleanField(default=False)

    # unique identifier of the email template
    template_key = models.CharField(max_length=255, unique=True)

    def get_rendered_template(self, tpl, context):
        return self.get_template(tpl).render(context)

    def get_template(self, tpl):
        return template.Template(tpl)

    def get_subject(self, subject, context):
        return subject or self.get_rendered_template(self.subject, context)

    def get_body(self, body, context):
        return body or self.get_rendered_template(self._get_body(), context)

    def get_sender(self):
        return self.from_email or settings.DEFAULT_FROM_EMAIL

    def get_recipient(self, emails, context):
        return emails or [self.get_rendered_template(self.to_email, context)]

    @staticmethod
    def send(*args, **kwargs):
        EmailTemplate._send(*args, **kwargs)

    @staticmethod
    def _send(template_key, context, subject=None, body=None, sender=None,
              emails=None, bcc=None, attachments=None):
        mail_template = EmailTemplate.objects.get(template_key=template_key)
        context = Context(context)

        subject = mail_template.get_subject(subject, context)
        body = mail_template.get_body(body, context)
        sender = sender or mail_template.get_sender()
        emails = mail_template.get_recipient(emails, context)

        if mail_template.is_text:
            return send_mail(subject, body, sender, emails, fail_silently=not
            settings.DEBUG)

        msg = EmailMultiAlternatives(subject, body, sender, emails,
                                     alternatives=((body, 'text/html'),),
                                     bcc=bcc
                                     )
        if attachments:
            for name, content, mimetype in attachments:
                msg.attach(name, content, mimetype)
        return msg.send(fail_silently=not (settings.DEBUG or settings.TEST))

    def _get_body(self):
        if self.is_text:
            return self.plain_text

        return self.html_template

    def __str__(self):
        return "<{}> {}".format(self.template_key, self.subject)

Example usage

Sending a notification to the admin about new expense request from an employee.

EmailTemplate.send('expense_notification_to_admin', {
    # context object that email template will be rendered with
    'expense': expense_request,
})

Or sending an invoice to the customer.

invoice_pdf = invoice.pdf_file.pdf_file_data
send_email(
    # a string such as 'invoice_to_customer'
    template_name,

    # context that contains Customer, Invoice and other objects
    ctx,

    # list of receivers i.e. ['customer1@example.com', 'customer2@example.com]
    emails=emails,

    # attached PDF file of the invoice
    attachments=[(invoice.reference, invoice_pdf, 'application/pdf')]
)

Running as a celery task

Emails can be sent via celery by adding a task to tasks.py:

from celery import task
from core.models import EmailTemplate


@task
def send_email(*args, **kwargs):
    return EmailTemplate.send(*args, **kwargs)

Enabling in an admin panel

admin.py can look like follows:

from django.contrib import admin
from core.models import EmailTemplate


class EmailTemplateAdmin(admin.ModelAdmin):
    list_display = ['template_key', 'subject', 'from_email', 'to_email']
    save_as = True

We will notify you about new posts every few weeks