Update: this post is for version 0.96 only since the trick is now outdated.
As many of you already the trunk version of Django carries a brand new infrastructure to handle HTML forms providing rendering capabilities and validation.
Its name is newforms but I guess it will soon become just forms replacing the current framework which will be removed in Django 1.0. The documentation is in evolution and right now does not cover all the stuff.
One of the top questions about newforms is how to upload a file and validate it in some way. It is imperative for our example to hook into the newforms’ validation system otherwise we won’t gain any advantage. Let’s move on, then.
A couple of days ago I read all the previous threads on django-users about the subject but nothing really came up so I dug into the bare code and I hacked some very straightforward (not sure if it’s a best practice) to handle the case.
In our sample we’re going to upload an image to a Django website validating the content (we want to be sure it’s an image), its width and height (we’re lazy and we want to spare CPU cycles instead of resizing on the fly) and its size.
The first thing we need it’s a model ready to save the data to a proper path on the server. For example:
[code lang="python"] from django.db import models
class Thing(models.Model): name = models.CharField(blank=False, null=False, maxlength=30) photo = models.ImageField(upload_to='images/things/', blank=True, null=True) [/code]
This way the Thing model has a mandatory name and an optional photo which will be uploaded to ${MEDIA_ROOT}/images/things (see FileField for details).
The form validation thingy is pretty simple: you create a form class with the needed fields, you pass the form object (unbounded) to the template from the view and when the browser issues a POST to the designed view you instantiate a bound form object and call form.is_valid() which will trigger full_clean() iterating through your fields and cleaning up the data. You can also write a clean_FIELDNAME method inside the form class to do custom validation. That’s exactly what we’re going to do later.
[code lang="python"] from django import newforms as forms from django.conf import settings from django.utils.translation import gettext as _
class ThingForm(forms.Form): name = forms.CharField(max_length=30, required=True, label=('Name of the thing')) photo = forms.Field(widget=forms.FileInput, required=False, label=('Photo'), help_text=_('Upload an image (max %s kilobytes)' % settings.MAX_PHOTO_UPLOAD_SIZE)) [/code]
Here we’ve defined a class representing our HTML form’s elements with the same mandatory name and a field (<input type=”file”>). Note: we tell the user we want a in image less than MAX_PHOTO_UPLOAD_SIZE bytes. Django has obviously no way to avoid an upload if the content exceeds the size we want so I strongly suggest to use Apache’s LimitRequestBody directive. Anyway we’re gonna check the file size for exercise in Django too but keep in mind that we risk to clog our memory because the file has already been uploaded to the server.
Django separates the data issued with a POST from the uploaded stuff. It has two dictionary-like objects to handle this. In request.POST we will find the normal data (like the name of the Thing in our case) and in request.FILES we’ll find the uploaded image and its metadata. See this page for further details.
The normal practice is, by the way, to instantiate the form object with request.POST as its content so, how we can stick to Django way and gain the benefits from the validation system if the data are separated?
That’s really simple. Let’s see the view to get to the point:
[code lang="python"] def thing_add(request): if request.method == 'POST': # hack to trigger validation even with file upload if 'photo' in request.FILES: try: img = Image.open(StringIO(request.FILES['photo']['content'])) request.FILES['photo']['dimensions'] = img.size except: request.FILES['photo']['error'] = True
new_data = request.POST.copy()
new_data.update(request.FILES)
form = ThingForm(new_data)
if form.is_valid():
clean_data = form.clean_data
t = Thing()
t.name = clean_data['name']
if clean_data['photo']:
photo = clean_data['photo']
t.save_photo_file(photo['filename'], photo['content'])
return HttpResponseRedirect('/some/where')
else:
form = ThingForm()
context = Context({'form': form})
return render_to_response('thing_add.html', context)
[/code]
Apart from the usual stuff we checked if the user has uploaded a file (photo is the name of the HTML element). If so we store the dimensions because we want to check if they are within our custom bounds. Image is a PIL module. The side effect of those lines is that meanwhile we’re yearning for the dimensions, PIL will check if the content is really a valid image so we do the dirty trick in the except clause to notify the validation system if something went wrong. We will see how to check if the uploaded data contains an image anyway (because we want only GIFs, JPGs or PNGs).
The magic lies in request.POST.update(request.FILES). After doing the PIL machinery we update request.POST object with the image data.
To trigger the custom validation (form.is_valid() starts it all) we have to create a method in the ThingForm named clean_FIELDNAME, in our case clean_photo:
[code lang="python"] def clean_photo(self): if self.clean_data.get('photo'): photo_data = self.clean_data['photo'] if 'error' in photo_data: raise forms.ValidationError(_('Upload a valid image. The file you uploaded was either not an image or a corrupted image.'))
content_type = photo_data.get('content-type')
if content_type:
main, sub = content_type.split('/')
if not (main == 'image' and sub in ['jpeg', 'gif', 'png']):
raise forms.ValidationError(_('JPEG, PNG, GIF only.'))
size = len(photo_data['content'])
if size > settings.MAX_PHOTO_UPLOAD_SIZE * 1024:
raise forms.ValidationError(_('Image too big'))
width, height = photo_data['dimensions']
if width > settings.MAX_PHOTO_WIDTH:
raise forms.ValidationError(_('Max width is %s' % settings.MAX_PHOTO_WIDTH))
if height > settings.MAX_PHOTO_HEIGHT:
raise forms.ValidationError(_('Max height is %s' % settings.MAX_PHOTO_HEIGHT))
return self.clean_data['photo']
[/code]
Let’s debunk it. First and foremost we check if the dictionary with the cleaned data contains the image (it means the user has uploaded something), if nothing is in there we return nothing itself (see the last line).
If something has been uploaded we check if that something is actually an image (because PIL says so), if not we raise a ValidationError.
After that we check if the image has a valid content type (we don’t want to deal with every type of image on the planet). If not we raise another ValidationError.
Next we check if the image it’s too big. Remember that at this time the data is in memory so do this if you really know what you’re doing, otherwise use LimitRequestBody.
Last step is to check the width and height and doing accordingly.
If everything went fine the image is ready to be stored to the server with model.save_FIELD_file(filename, content).
You can later use get_FIELD_XYZ() methods to retrieve info about the stored file.
That’s it I guess. Remember to use enctype=”multipart/form-data” in the <form method=”post”> tag!
HTH




16 Comments
please upload this to http://www.djangosnippets.org/
A little glitch in the last block of code. height instead of width :
if height > settings.MAX_PHOTO_HEIGHT: raise forms.ValidationError(_(‘Max height is %s’ % settings.MAX_PHOTO_HEIGHT))thanks for the post.
@Doug: I’ll try to do that ASAP
@Yoan: Thanks for the fix!
Hey,
I’ve finally gotten this to work. Have a web app and quite a few Alpha testers (classowl.com), and a lot of them use IE. Found one major problem, you need to update your code so it works in IE.
‘x-png’, ‘pjpeg’
Add those two to the allowed mime types, that’s how IE interprets lots of JPGs and PNGs.
Thank you for the great script, it’s helped me out a lot.
Thanks a lot Sam! Didn’t know that
I’m gonna fix my code ASAP.
ps. http://msdn.microsoft.com/work.....ndix_a.asp
This contains some info about mime types
Just note that it might not be such a great idea to alter request data directly as in: request.POST.update(request.FILES)
but rather: new_data = request.POST.copy() new_data.update(request.FILES)
@Mikko: fixed thanks
From version 0.96 use “cleaned_data” instead of “clean_data”
http://code.djangoproject.com/changeset/5237
There is a “best practice” file upload script on http://www.djangosnippets.org/snippets/95/
Would I need to add another save function after the “save_photo_file” function in order to save the t.name?
@Priyesh: not at all. You can just call t.save() after modifying t.name
Thanks man, you help me a lot!
Looks a little different for the latest version of DJANGO… or whichever one I have in svn…
They now check the images for you (tested it) So you can avoid the cool hack.
In order to access the filename and content, treat them as fields, ie:
change:
t.save_photo_file(photo['filename'], photo['content'])
to:
t.save_photo_file(photo.filename, photo.content)
Nonetheless, great post! — I was searching for ages on making just basic photo uploading work through the new forms, and this is the only site that was helpful. Also, if your urls are screwed up after that… check out your settings file values for MEDIA_ROOT, MEDIA_URL, MEDIA_ADMIN – and play with those until it works (also whatever you have as ‘upload_to’ in your model for the ImageField)
Thanks for the tip
This post is outdated now – if you’re using the latest django, do not follow this post.
It is quite helpful for 0.96, however
2 Trackbacks/Pingbacks
[...] Django image upload and validation. The author uses a model for the file and its related data. The uploaded file is saved by calling the save_FOO_file method. (This method is automatically provided by Django for fields declared as models.ImageField or models.FileField in the model. See the db-api documentation.) [...]
[...] A song for the lovers » Django, image uploading, validation and newforms One of the top questions about newforms is how to upload a file and validate it in some way. It is imperative for our example to hook into the newforms’ validation system otherwise we won’t gain any advantage. Let’s move on, then. (tags: django forms newforms image upload validation file tutorial) [...]