Django and AJAX image uploads
Table of contents:
- Server side (Django)
- Client side
- Graceful degradation
The upload form, empty and ready for action:
Browsing for an image:
Uploading the image (in progress):
The image is uploaded:
Deleting the image would show a similar progress block as uploading:
In this post, we'll go through how to get AJAX uploads to work with Django, including:
- csrf protection with Django's forms
- uploading files in Django
- thumbnail generation with PIL
- cross-browser uploading of files through AJAX
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.
I created an app called
upload with an ImageAttachment model, like so:
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:
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.
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
csrfmiddlewaretokenhidden 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
<form>and creates an
<iframe>to which that form posts.
These two pretty much summarize the process:
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
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!