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:
- 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)
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.
- 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))
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:
- 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)
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:
- 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’]
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

