Django and AJAX image uploads
Table of contents:
Demo
-
Screenshots:
-
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:
-
Summary
In this post, we'll go through how to get AJAX uploads to work with Django, including:
- csrf protection with Django's forms
- graceful degradation (see also unobtrusive JavaScript)
- 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.
Model
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:
Form
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 thecsrfmiddlewaretoken
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:
-
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!