Hunter Ford Hunter Ford

HTML Emails with Inline Images in Django

For an upcoming party I was having, I decided to create an e-vite like application to send out invitations and allow people to respond via the site. I wanted to include an image in the email, and there just isn't a built-in utility inside Django. So I decided to extend the EmailMultiAlternatives class. Most of the code should be pretty self-explanatory. Also attached is an example of invocation.

import os.path
import re

from email.MIMEBase import MIMEBase

from django.conf import settings
from django.core.mail import EmailMultiAlternatives, SafeMIMEMultipart

class EmailMultiRelated(EmailMultiAlternatives):
    """
    A version of EmailMessage that makes it easy to send multipart/related
    messages. For example, including text and HTML versions with inline images.
    """
    related_subtype = 'related'

    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
            connection=None, attachments=None, headers=None, alternatives=None):
        # self.related_ids = []
        self.related_attachments = []
        return super(EmailMultiRelated, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers, alternatives)

    def attach_related(self, filename=None, content=None, mimetype=None):
        """
        Attaches a file with the given filename and content. The filename can
        be omitted and the mimetype is guessed, if not provided.

        If the first parameter is a MIMEBase subclass it is inserted directly
        into the resulting message attachments.
        """
        if isinstance(filename, MIMEBase):
            assert content == mimetype == None
            self.related_attachments.append(filename)
        else:
            assert content is not None
            self.related_attachments.append((filename, content, mimetype))

    def attach_related_file(self, path, mimetype=None):
        """Attaches a file from the filesystem."""
        filename = os.path.basename(path)
        content = open(path, 'rb').read()
        self.attach_related(filename, content, mimetype)

    def _create_message(self, msg):
        return self._create_attachments(self._create_related_attachments(self._create_alternatives(msg)))

    def _create_alternatives(self, msg):
        for i, (content, mimetype) in enumerate(self.alternatives):
            if mimetype == 'text/html':
                for filename, _, _ in self.related_attachments:
                    content = re.sub(r'(?<!cid:)%s' % re.escape(filename), 'cid:%s' % filename, content)
                self.alternatives[i] = (content, mimetype)

        return super(EmailMultiRelated, self)._create_alternatives(msg)

    def _create_related_attachments(self, msg):
        encoding = self.encoding or settings.DEFAULT_CHARSET
        if self.related_attachments:
            body_msg = msg
            msg = SafeMIMEMultipart(_subtype=self.related_subtype, encoding=encoding)
            if self.body:
                msg.attach(body_msg)
            for related in self.related_attachments:
                msg.attach(self._create_related_attachment(*related))
        return msg

    def _create_related_attachment(self, filename, content, mimetype=None):
        """
        Convert the filename, content, mimetype triple into a MIME attachment
        object. Adjust headers to use Content-ID where applicable.
        Taken from http://code.djangoproject.com/ticket/4771
        """
        attachment = super(EmailMultiRelated, self)._create_attachment(filename, content, mimetype)
        if filename:
            mimetype = attachment['Content-Type']
            del(attachment['Content-Type'])
            del(attachment['Content-Disposition'])
            attachment.add_header('Content-Disposition', 'inline', filename=filename)
            attachment.add_header('Content-Type', mimetype, name=filename)
            attachment.add_header('Content-ID', '<%s>' % filename)
        return attachment

Invocation

msg = EmailMultiRelated('Subject', 'Plain text version', 'John Foo <john@foo.com>', ['Jane Bar <jane@bar.com'])

html = '<html><body><p>This is my nicely <strong>formatted</strong> message. <a href="mailto:john@foo.com">Email</a> me back.</p><img src="inline.jpg"></body></html'

msg.attach_alternative(html, 'text/html')

for image in event.attachment_set.all():
    msg.attach_related_file(image.file.path)

ics_data = '''BEGIN:VCALENDAR
PRODID:-//HunterFord//EN
VERSION:2.0
BEGIN:VEVENT
URL:http://example.com
DTSTART:20101001T200000
DTEND:20101001T235959
SUMMARY:Fall is in the Air
ORGANIZER;CN=John Foo:MAILTO:john@foo.com
LOCATION:Home
DESCRIPTION:
PRIORITY:3
END:VEVENT
END:VCALENDAR'''

msg.attach('event.ics', ics_data, 'text/calendar')
msg.send()

Django Custom Model Manager Chaining

Let's say you have a custom model manager like this:

from datetime import datetime

from django.db import models

class PostManager(models.Manager):
    def by_author(self, user):
        return self.filter(user=user)

    def published(self):
        return self.filter(published__lte=datetime.now())

class Post(models.Model):
    user = models.ForeignKey(User)
    published = models.DateTimeField()

    objects = PostManager()

But let's say you are want to filter those results that are both published and by a certain author. There is no built-in mechanism that will allow you to chain the two together.

So for example, the following will fail since by_author returns a QuerySet:

Post.objects.by_author(user=request.user).published()

There are a few discussions on the matter:

But using Python mixins I've developed what I think to be a better way:

from django.db.models.query import QuerySet

class PostMixin(object):
    def by_author(self, user):
        return self.filter(user=user)

    def published(self):
        return self.filter(published__lte=datetime.now())

class PostQuerySet(QuerySet, PostMixin):
    pass

class PostManager(models.Manager, PostMixin):
    def get_query_set(self):
        return PostQuerySet(self.model, using=self._db)

It keeps with DRY principles and works just like you expect Django's ORM to work.

Django Messaging for AJAX Calls Using Jquery

The messaging contrib app for Django has always been been tied to a user, which has prevented me from using it in any of my apps. Now that the Django 1.2 Alpha has been released, I've been able to play with it, and feel good about using it. My sites usually have a mix of AJAX and traditional HTTP requests. So figuring out a good solution to handle messages for the AJAX requests, while maintaining consistency in interacting with the API was important to me. Here's my solution for handling messages in AJAX requests.

First we need a middleware that will detect if the request is an AJAX request. I always use JSON, but if you mix the data types, you might have to add some additional logic here. The middleware adds to the JSON any messages that we might have added in the view.

import simplejson as json

from django.contrib import messages

class AjaxMessaging(object):
    def process_response(self, request, response):
        if request.is_ajax():
            if response['Content-Type'] in ["application/javascript", "application/json"]:
                try:
                    content = json.loads(response.content)
                except ValueError:
                    return response

                django_messages = []

                for message in messages.get_messages(request):
                    django_messages.append({
                        "level": message.level,
                        "message": message.message,
                        "extra_tags": message.tags,
                    })

                content['django_messages'] = django_messages

                response.content = json.dumps(content)
        return response

Make sure to add the new middleware to settings.py.

'apps.main.middleware.AjaxMessaging'

So if we have a view that responds to an AJAX request, we can add a message the same way we would for a traditional HTTP response:

if success:
    messages.success(request, "The object has been modified.")
else:
    messages.error(request, "The object was not modified.")

We need a place to put the messages in our template:

<ul id="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>

We'll be using a global ajaxComplete handler, so we'll have to use a JSON parser. Simply include it with the rest of our javascript. jquery.json.js

And finally the handler, which simply loops through all the Django messages, and fades them out after three seconds.

function addMessage(text, extra_tags) {
    var message = $('<li class="'+extra_tags+'">'+text+'</li>').hide();
    $("#messages").append(message);
    message.fadeIn(500);

    setTimeout(function() {
        message.fadeOut(500, function() {
            message.remove();
        });
    }, 3000);
}

$(document).ready(function() {
    $('#messages').ajaxComplete(function(e, xhr, settings) {
        var contentType = xhr.getResponseHeader("Content-Type");

        if (contentType == "application/javascript" || contentType == "application/json") {
            var json = $.evalJSON(xhr.responseText);

            $.each(json.django_messages, function (i, item) {
                addMessage(item.message, item.extra_tags);
            });
        }
    }).ajaxError(function(e, xhr, settings, exception) {
        addMessage("There was an error processing your request, please try again.", "error");
    });