Uploading files via newforms in Django made easy.


I have already described creating custom fields with Django and shown how easy it can get. Today, I am going to show how to manage file uploading through newforms. I expended a good part of my morning and afternoon today reading up on Django and asking for help in #django on irc.freenode.net. Many thanks to the folks in #django for helping me out.

I will take the aid of code snippets as much as I can to describe the process. That way, you can not only grasp it quickly and place it all in a useful context, but can also use my code and apply it in applications.

First of all, I have Django configured on the development webserver that ships with it. Therefore, in order for Django to serve static contents, such as images, a couple of settings need be taken care of.

I have a directory, ‘static/’, in my root Django project folder in which I keep all the static content to be served. Within settings.py file, it is important to correctly define the MEDIA_ROOT and MEDIA_URL variables to point to that static/ directory. I have the two variables defined thusly:

ayaz@laptop:~$ grep 'MEDIA_' settings.py
MEDIA_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static/")
MEDIA_URL = 'http://192.168.1.2:8000/static/'

The structure of the static/ directory looks like the following:

ayaz@laptop:~$ ls -l static/
total 20
drwxr-xr-x 3 ayaz ayaz 4096 2007-06-28 15:34 css
drwxr-xr-x 3 ayaz ayaz 4096 2007-07-06 21:51 icons
drwxr-xr-x 3 ayaz ayaz 4096 2007-07-06 21:51 images
drwxr-xr-x 3 ayaz ayaz 4096 2007-07-21 15:57 license
drwxr-xr-x 3 ayaz ayaz 4096 2007-07-21 11:41 pictures

For Django to serve static content, it must be told via pattern(s) in URLconf at which URL path the static content is available. The following are relevant lines from urls.py (note that MEDIA_PATH is defined to point to the same directory as does MEDIA_ROOT in settings.py):

ayaz@laptop:~$ head -5 urls.py
MEDIA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static/")
urlpatterns = patterns('',
  (r'^static/(?P.*)$', 'django.views.static.serve',
      {'document_root': MEDIA_PATH, 'show_indexes': True}),

That is all you need to do to enable Django to serve static content. Next up is the Model that I am using here as an example. I have the model DefaulProfile, which has two fields, the last of which is of interest here. The ‘picture’ field, as you can see below, is of type models.ImageField. Note the ‘upload_to’ argument (Consult the Model API for documentation on the various Model Fields and their supported arguments). It points to ‘pictures/’ which will, when Django processes it, be appended to the end of the path saved in MEDIA_ROOT. Also note that the ‘picture’ field is mandatory.

ayaz@laptop:~$ cat models.py
from django.db import models
from django.contrib.admin.models import User

class DefaultProfile(models.Model):
  user = models.ForeignKey(User)
  picture = models.ImageField("Personal Picture", upload_to="pictures/")

Now, let’s flock on to the views. I have already described the PictureField, so I will skip it.

ayaz@laptop:~$ cat views.py
from django import newforms as forms
from django.contrib.admin.models import User
from myapp.mymodel import DefaultProfile

class PictureField(forms.Field):
  def clean(self, value):
    if not value:
      return
    file_exts = ('.png', '.jpg', '.jpeg', '.bmp',)
    from os.path import splitext
    if splitext(value)[1].lower() not in file_exts:
      raise forms.ValidationError("Only following Picture types accepted: %s"
        % ", ".join([f.strip('.') for f in file_exts]))
    return value

‘UserForm’ is a small form model class, which describes the only field I am going to use, ‘picture’. Note the widget argument.

class UserForm(forms.Form):
  picture = PictureField(label="Personal Picture", widget=forms.FileInput)

From now starts the meat of it all. Note request.FILES. When you are dealing with multipart/form-data in forms (such as in this case where I am trying to accept an image file from the client), the multipart data is stored in the request.FILES dictionary and not in the request.POST dictionary. Note that the picture field data is stored within request.FILES[‘picture’].

The save_picture() function simply retrieves the ‘filename’, checks to see if the multipart data has the right ‘content-type’, and then saves the ‘content’ using a call to the object’s save_picture_file() method (this method is documented here).

def some_view(request):

  def save_picture(object):
    if request.FILES.has_key('picture'):
      filename = request.FILES['picture']['filename']
      if not request.FILES['picture']['content-type'].startswith('image'):
        return
    filename = object.user.username + '__' + filename
    object.save_picture_file(filename, request.FILES['picture']['content'])

  post_data = request.POST.copy()
  post_data.update(request.FILES)
  expert_form = UserForm(post_data)

  if expert_form.is_valid():
    default_profile = DefaultProfile()
    save_picture(default_profile)
    default_profile.save()

If you look closely, you’ll notice I created a copy of request.POST first, and then updated it with request.FILES (the update() method is bound to dictionary objects and merges the key-value pairs in the dictionary given in its argument with the source dictionary). That’s pretty simple to understand. When an object of UserForm() is created, it is passed as its first argument a dictionary, which is usually request.POST. This actually creates a bound form object, in that the data is bound to the form. So when the method is_valid() is called against a bound form object, Django’s newforms’ validators perform field validation against all the fields in request.POST that are defined in the form definition, and raise ValidationError exception for each field which does not apparently have the right type of data in it. Now, if you remember, the picture field was created to be a mandatory field. If we didn’t merge the request.FILES dictionary with a copy of request.POST, the is_valid() method would have looked into request.POST and found request.POST[‘picture’] to be missing, and therefore, would have issued an error. This is a nasty, subtle bug, that, I’ve to admit, wasted hours trying to figure out — is_valid() is looking for the field ‘picture’ in request.POST when in reality, for multipart form-data forms, the ‘picture’ field is stored within request.FILES, but since request.FILES was never bound to the form object, and instead request.POST was, it would never find ‘picture’. So, when request.FILES and request.POST are merged together, the resultant object has all the required fields, and is_valid() correctly finds them and does not contain, provided there is no errant data in any of the fields.

I sure do hope the explanation is clear enough. Also, do take care the the “form” tag in the corresponding template has the enctype argument defined as “enctype=multipart/form-data”.

7 thoughts on “Uploading files via newforms in Django made easy.

  1. This was a great help. I think you could make it simpler though. Instead of combining the request.POST and request.FILES dictionaries, just pass them both to the form:

    form = UserForm(request.POST, request.FILES)

    You will also need to change the clean function in the PictureField class. The variable “value” will be a dictionary, so change the function to this:

    def clean(self, value):
    if not value:
    return
    file_exts = (’.png’, ‘.jpg’, ‘.jpeg’, ‘.bmp’,)
    from os.path import splitext
    if splitext(value[‘filename’])[1].lower() not in file_exts:
    raise forms.ValidationError(”Only following Picture types accepted: %s” % “, “.join([f.strip(’.’) for f in file_exts]))
    return value

    Keith

  2. Thanks for the comment, Keith. It’s much appreciated.

    I did not know request.FILES could be combined with request.POST like that when I wrote the blog. I did find it out later on.

    Also, with the newer releases of Django, all the manual handling of request.FILES is unnecessary, as the newforms framework natively supports methods and helpers to deal with all that. I have not checked that out yet — but I’ve been looking forward to it.

    :)

  3. Thanks for the info.

    Just a heads up; there’s a typo in the following:

    if expert_form.is_valid():
    default_profile = DefaultProfile()
    save_picture(defaul_profile)
    default_profile.save()

    ‘defaul_profile’ in the third line needs to be changed to ‘default_profile’

  4. Hi
    Your post is a God’s gift to me for the situation I m in..! I wanted to know How we could generate some URL(which could be shared with anyone anywhere) for the uploaded file and also assign some time constraint for the URL, i.e shuld exist only for a specific amount of time. Please do respond :)
    Regards,
    FirstRock

  5. The opportunities that blessed the accounting industry remain high.
    Each partner reports his or her share of the LLP’s profit or loss on form 1065, schedule K. If before you need to go to school to be able to have your degree or graduate now in this era you need not go to school you can just stay and still earn a degree.

  6. They usually do not use complicated logics and other syntaxes which their high end counterparts
    use and are therefore easily understood by common man quicker.

    The professional bookkeepers and other finance professionals that are involved
    in providing their best services during such times come well equipped with sufficient
    knowledge and a greater understanding regarding the requirements
    of bookkeeping process for every type of industry and organization.
    How they will be getting their bills and when they need to make the payments and also explaining regarding
    their payment options.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s