Filebaby list view

This tutorial demonstrates the use of the FileField model field to save user submitted files in a Django 1.5 web application using Django’s generic Class-based Views (CBVs).

Goals

This code will demonstrate how to handle user submitted files using Django’s generic Class-based views (CBVs).

Your users will be able to:

  • Download file(s) from a list of recently uploaded files
  • Anonymously upload files for immediate publishing

Caveat

This tutorial does not cover access control; anonymous users can upload files and publish them immediately. Control over who uploads and downloads files using generic CBVs is covered in the Django documentation.

This app was not tested on any Windows operating systems.

Prerequisites

  • Basic knowledge of Django
  • Basic knowledge of Python
  • Basic knowledge of Unix
  • Python 2.7 recommended (installed and working)
  • Unix-like operating system (Linux, Mac, FreeBSD, etc)
  • Virtualenv and Virtualenvwrapper are strongly recommended

You can follow along by downloading the completed code and running it, or by visiting the repository.

Get the source code

This entire tutorial is available on Bitbucket:


https://bitbucket.org/kelvinwong_ca/kelvinwong_ca_blogcode/src

Examine the directory named:

./django_filefield_tutorial/

Or you can download a tarball here:


https://bitbucket.org/kelvinwong_ca/kelvinwong_ca_blogcode/downloads

Get the download named:

django_filefield_tutorial.tar.gz

Uncompress it:

$ tar zxvf django_filefield_tutorial.tar.gz

Make a new virtualenv and install the requirements with ‘pip‘:

$ pip install -r uploadering/requirements.txt

When you have installed the project requirements, open the uploadering directory:

$ cd uploadering

Ensure the code works by running its tests.

Run the tests (optional)

I have included a few simple tests that you can use to test your application:

uploadering/filebaby/tests.py

The tests can be run using Django’s test runner:

$ python manage.py test filebaby
Creating test database for alias 'default'...
..........
----------------------------------------------------------------------
Ran 10 tests in 0.158s

OK
Destroying test database for alias 'default'...

Running these tests will verify that things will work as expected for the rest of the tutorial.

Big Picture Overview

Try running the server. Open the ‘uploadering’ directory, initialize the database and run the development server using the ‘manage.py’ script:

$ python manage.py syncdb

...setup databases stuff...

$ python manage.py runserver

Open the ‘/add’ URL (hint: try http://localhost:8000/add). You should see the following.

filebaby add file inset

When your user clicks the ‘add file’ button on the app it loads the ‘/add’ URL. An empty form is presented.

The user attaches a file and uploads the file back to the ‘/add’ URL. That file is received as a POST with file data and Django processes it. When processing is completed, the class-based view emits a redirect to the user’s browser asking it to load the success URL which in this case is the home page. A success message is displayed.

filebaby success inset

Where does Django store your uploaded files?

You need to tell Django where to store the uploaded files. This location is assigned in the ‘MEDIA_ROOT’ variable. If you are in the project’s root folder (named uploadering) open the settings.py file located at:

uploadering/settings.py

Django expects a full absolute pathname. You could put in a text string, but I usually set up a PROJECT_ROOT variable which is calculated relative to the directory containing all the project files. Put it at the top of the settings file somewhere.

 
# uploadering/settings.py
 
import os  # Put this up top, maybe line #2
#
# ...somewhere lower in the file...
#
PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
#
# This contains the full absolute path to the project directory
# ie. /home/myuser/webapps/uploadering/

Once configured, you can use it to build your absolute pathnames in your project.

 
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
# If PROJECT_ROOT is '/home/myuser/webapps/uploadering/' then
# MEDIA_ROOT is '/home/myuser/webapps/uploadering/userfiles/'
#
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'userfiles')
 
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
#
MEDIA_URL = '/files/'  # Note they don't have to be identical names

Now that your settings.py is configured, any user uploaded files will reside in this directory:

userfiles/

This is the directory tree structure relative to your project root (the README file marks the user uploads folder):

uploadering/
├── filebaby
├── uploadering
└── userfiles
    └── README

Now let’s look at the model for the user files.

FileField on the model sets the upload directory format

Our FilebabyFile data model has a property named ‘f’ for ‘file’. For your own application the name should be more descriptive, but for this tutorial a simple ‘f’ is good enough. Open this file and find the FileField model field:

filebaby/models.py

Find the FileField model field:

# filebaby/models.py
 
class FilebabyFile(models.Model):
    """This holds a single user uploaded file"""
    f = models.FileField(upload_to='.')

The FileField model field has one required attribute named ‘upload_to’. You must set this. You have three choices for this attribute: a string containing a period (as shown), a ‘strftime’ format string or a custom callable (usually a function but it can be any callable). I’m going to leave this as a dot string. This will ungraciously dump all your user submitted files into the MEDIA_ROOT directory.

For your own application, you might make this a named user directory, a hashed string or something more appropriate than a dot string. To do this, you can set the ‘upload_to’ parameter to use a callback.

FilebabyForm model form class is a standard model form

The FilebabyForm is a regular ModelForm that obtains its properties from the model designated in the inner class Meta. It is located at:

filebaby/forms.py

If you open this file you will be bored to tears.

# filebaby/forms.py
 
class FilebabyForm(forms.ModelForm):
    """Upload files with this form"""
    class Meta:
        model = FilebabyFile

There is nothing remarkable about this class so I won’t dwell on it. More information on ModelForms can be found on the Django web site.

FileAddView class-based view handles successful uploads

The FileAddView uses the generic FormView class-based view provided by Django. Examine the application views file:

# filebaby/views.py
 
class FileAddView(FormView):
 
    form_class = FilebabyForm
    success_url = reverse_lazy('home')
    template_name = "filebaby/add.html"
 
    def form_valid(self, form):
        form.save(commit=True)
        messages.success(self.request, 'File uploaded!')
        return super(FileAddView, self).form_valid(form)

You can see that I have overridden the following class properties:

  1. form_class – The form class used by the view
  2. success_url – Destination on successful upload
  3. template_name – Template used with the upload form

I have also overridden this method:

  1. form_valid – For processing successful uploads

The form_class is the FilebabyForm we discussed earlier. The FormView expects a form and one must be provided; here it is.

The success_url is where the user ends up when the file is uploaded successfully. I have used named URLs in my URLConf scheme and therefore I used the lazy version of the URL reverse function. This is necessary since the URLs are not loaded when the views are instantiated. If you see a NameError exception then you might have used the non-lazy version of ‘reverse’:

NameError at / name 'reverse' is not defined

The template_name is the template that contains a groovy form. It might be so groovy that it doesn’t work on early versions of Internet Explorer – I didn’t test it a whole lot on Windows and you’re not using IE7 right? Right? Check it out:

filebaby/templates/filebaby/add.html

If you find the template too groovy and difficult to follow, I have included a much simpler boring one as well (it can be swapped out – try it):

filebaby/templates/filebaby/add-boring.html

The boring version contains only the important parts of the form: enctype setting, file input and errors placeholder, submit button and cross site request forgery (CSRF) token. That’s it.

<form action="/add" method="post" enctype="multipart/form-data">
<input name="f" type="file" id="file" />
<input type="submit" id="submit" value="Submit File" />
{% csrf_token %}
</form>

The form_valid method runs when the form passes validation. Since we are uploading a file, this method is activated when a file is received. The form instance is passed to the method and we need to save it to place the file in the MEDIA_URL directory. A success message is displayed.

Mapping URLs to Views in URLConf

The add view is mapped to a URL in the URLConf file at:

# uploadering/urls.py
url(r'^add$', FileAddView.as_view(), name='filebaby-add'),

The path ‘/add’ gets mapped to the FileAddView. A helpful name is provided to use the reverse helper in the templates. Naming your URLs is not required but I do it.

Serving the user submitted files in development

In order to serve the user submitted files while you are developing your site, you need to add some code to the URLConf.

# /uploadering/uploadering/urls.py
 
from django.conf.urls.static import static
from django.conf import settings
 
urlpatterns = patterns('',
    # ...
    # URL mappings
    # ...
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Adding the static function to the end of the urlpatterns tells the development server to map the MEDIA_URL to the files in the MEDIA_ROOT. Using our settings, a browser request for:

/files/myfile.txt

Yields a file located at:

/home/myuser/webapps/uploadering/userfiles/myfile.txt

Listing your uploaded files with a ListView

The home page uses a generic CBV called ListView to display the list of files. The implementation is standard except for the default query which orders by largest primary key (in lieu of a date value).

class FileListView(ListView):
 
    model = FilebabyFile
    queryset = FilebabyFile.objects.order_by('-id')
    context_object_name = "files"
    template_name = "filebaby/index.html"

I won’t dwell on this since it is outside the scope of this tutorial. It does have working pagination and a Foundation 4 compatible Django messages implementation which might be of interest to some.

That’s it for the tutorial. Go build your app now.

Thought questions

  • Have you ever implemented an upload app via a FileField using view functions?
  • Do you find the generic Class-based View (CBV) method an improvement over functional views?
  • What URL scheme do you prefer for your file uploads and why?