Django and AJAX image uploads

Table of contents:

Demo

  • Screencast

  • Screenshots:

    • The upload form, empty and ready for action:

      Empty upload form

    • Browsing for an image:

      Browsing for an image

    • Uploading the image (in progress):

      Uploading the image

    • The image is uploaded:

      Uploaded image

    • Deleting the image would show a similar progress block as uploading:

      Delete the image

Summary

In this post, we'll go through how to get AJAX uploads to work with Django, including:

Note: I'm planning to add upload progress. If you can't wait for that post (understandably), there are several ways to go about it:

  • If your site isn't using multiple webheads, you can just ask the webhead to get you the size of what's been uploaded so far. Since Django can read in chunks, it can tell you how much has been processed. See this post for implementation ideas.
  • Or, regardless of the server setup, you can use the File API (in Firefox and Chrome) - easier, cleaner, no server-side interaction required.
  • Other multi-webhead approaches: writing progress to a file shared among them, or saving directly to a shared folder and e.g. returning the size uploaded so far.

Server side (Django)

First, we'll look at how the server handles files sent to it.

Model

I created an app called upload with an ImageAttachment model, like so:

apps/upload/models.py:

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db import models


class ImageAttachment(models.Model):
    """A tag on an item."""
    file = models.ImageField(upload_to=settings.IMAGE_UPLOAD_PATH)
    thumbnail = models.ImageField(upload_to=settings.THUMBNAIL_UPLOAD_PATH)
    creator = models.ForeignKey(User, related_name='image_attachments')
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()

    content_object = generic.GenericForeignKey()

    def __unicode__(self):
        return self.file.name

This represents an image attached to a piece of content (using a generic foreign key). Pretty basic stuff. The form is ridiculously simple:

Form

apps/upload/forms.py:

from django import forms


class ImageUploadForm(forms.Form):
    """Image upload form."""
    image = forms.ImageField()

View (uploading image, saving to disk)

The view is a bit more complicated, so I won't go into the details. But you can have a look at the entire app and contact me if you have questions. Basically, the view does the file upload as you see in Django's documentation. The function create_image_attachment deals with the part about saving a file to disk.

Generating the thumbnail with PIL

There is also a task for generating thumbnails, which is offloaded from the web server thread to improve performance. If you don't need that, you can just call generate_thumbnail directly, it's defined here.

Client side

Now, the magical JavaScript!

We're using jQuery on SUMO, so I wrote two jQuery extensions:

  • jQuery.fn.ajaxSubmitInput(options) -- wraps an <input type="file"> in a <form> and creates an <iframe> to which that form posts. To get around Django's csrf protection, it also copies the csrfmiddlewaretoken hidden input into the form. You can't clone a file input for security reasons (nor can you change or access its value), so you need to wrap it in a form.
  • jQuery.fn.wrapDeleteInput(options) -- wraps an input<type="submit"> in a <form> and creates an <iframe> to which that form posts.

These two pretty much summarize the process:

  1. when the user changes the value of the file input, post the form

    • show some progress while the file is uploading
    • once the file is done uploading, show a thumbnail of the image

    • also create the delete input and wrap it in the form using wrapDeleteInput()

    • when the user clicks on the delete button, post the action

    • show some progress while the file is being deleted

A note about graceful degradation

To degrade gracefully, you want to post the file input to whatever view you're including it to. And you can just do something like:

def some_view(request):
    # ...
    # NOJS: upload image
    if 'upload_image' in request.POST:
        upload_images(request, obj)
    # ...

Thanks for reading, hope it helps!