Wednesday, October 10, 2012

Three Approaches to Handling Static Files in Django

I had a really great (and lengthy) pair programming session today with a coworker during which we spent a bit of time going over a couple of different approaches for dealing with static files in Django, so I thought I'd document and share this information while it's fresh in my mind.

First, a little background. If you're not familiar with Django it was originally created for a newspaper web site, specifically the Lawrence Journal-World, so the approach to handling what in the Django world are called "static files" -- meaning things like images, JavaScript, CSS, etc. -- is based on the notion that you might be using a CDN so you should have maximum flexibility as to where these files are located.

While the flexibility is indeed nice, if you're used to a more self-contained approach it takes a little getting used to, and there are a few different ways to configure your Django app to handle static files. I'm going to outline three approaches, but using different combinations of things and other solutions of which I may be unaware there are certainly more ways to handle static files than what I'll outline here. (And as I'm still relatively new to Django, if I'm misunderstanding any of this I'd love to hear where I could improve any of what I'm sharing here!)

One other caveat -- I'm focusing here on STATIC_URL and ignoring MEDIA_URL but the approach would be the same.

Commonalities Across All Approaches

First, even though it may not strictly be required depending on which approach you take for handling static files, since you wind up needing to use this for other reasons, we'll use django.template.RequestContext in render_to_response as opposed to the raw request object. This is required if you want access to settings like MEDIA_URL and STATIC_URL in your Django templates. For more details about RequestContext, the TEMPLATE_CONTEXT_PROCESSORS that are involved behind the scenes, and the variables this approach puts into your context, check the Django docs.

I'm also operating under the assumption that the static files will live in a directory called static that's under the main application directory inside your project directory (i.e. the directory that has your main settings.py file in it). Depending on the approach you use you may be able to put the static directory elsewhere, but unless stated otherwise, that's where the directory is assumed to be. (Note that if you store static files on another server entirely, such as using a CDN, STATIC_URL can be a full URL as opposed to a root-relative URL like /static/)

Also in all examples it's assumed that the STATIC_URL setting in the main settings.py file is set to '/static/'

Approach One (Basic): Use STATIC_URL Directly in Django Templates

This is the simplest approach and may be all you need. With STATIC_URL set to '/static/' in the main settings.py file, all you really have to worry about is using RequestContext in your view functions and then referencing {{ STATIC_URL }} in your Django templates.

Here's a sample views.py file:

from django.shortcuts import render_to_response
from django.template import RequestContext

def index(request, template_name='index.html'):
    return render_to_response(template_name, context_instance=RequestContext(request))

By using RequestContext the STATIC_URL variable will then be available to use in your Django templates like so:

<html>
<head>
    <script src="{{ STATIC_URL }}scripts/jquery/jquery-1.8.1.min.js"></script>
</head>
<body>
    <img src="{{ STATIC_URL }}images/header.jpg" />
</body>
</html>

That's all there is to it. Again, since /static/ will be relative to the root of the main application directory in your project it's assumed that the static directory is underneath your main application directory for this example, and obviously in the case of the example above that means that underneath the static directory you'd have a scripts and images directory.

Approach Two: Use a URL Pattern, django.views.static.serve, and STATICFILES_DIRS

In this approach you leverage Django's excellent and hugely flexible URL routing to set a URL pattern that will be matched for your static files, have that URL pattern call the django.views.static.serve view function, and set the document_root that will be passed to the view function to a STATICFILES_DIRS setting from settings.py. This is a little bit more involved than the first approach but gives you a bit more flexibility since you can place your static directory anywhere you want.

The approach I took with this method was to set a CURRENT_PATH variable in settings.py (created by using os.path.abspath since we need a physical path for the document root) and leverage that to create the STATICFILES_DIRS setting. Here's the relevant chunks from settings.py:

import os
CURRENT_PATH = os.path.abspath(os.path.dirname(__file__).decode('utf-8')).replace('\\', '/')
...
STATICFILES_DIRS = (
    os.path.join(CURRENT_PATH, 'static'),
)

Note that the replace('\\', '/') bit at the end of the CURRENT_PATH setting is to make sure things work on Windows as well as Linux.

Next, set a URL pattern in your main urls.py file:

from django.conf.global_settings import STATICFILES_DIRS
...
urlpatterns = patterns('',
    url(r'^static/(?P<path>.*)$', 'django.views.static.serve', {'document_root': STATICFILES_DIRS}),
)

And then in your Django templates you simply prefix all your static assets with /static/ as opposed to using {{ STATIC_URL }} as a template variable. Even though you're specifying /static/ explicitly in your templates, you still have flexibility to put these files wherever you want since the URL pattern acts as an alias to the actual location of the static files.

Approach Three: Use staticfiles_urlpatterns and {% get_static_prefix %} Template Tag

django.contrib.staticfiles was first introduced in Django 1.3 and was designed to clean up, simplify, and create a bit more power for static file management. This approach gives you the most flexibility and employs a template tag instead of a simple template variable when rendering templates.

First, in settings.py we'll do the same thing we did in the previous approach, namely setting a CURRENT_PATH variable and then use that to set the STATICFILES_DIRS variable:


import os
CURRENT_PATH = os.path.abspath(os.path.dirname(__file__).decode('utf-8')).replace('\\', '/')
...
STATICFILES_DIRS = (
    os.path.join(CURRENT_PATH, 'static'),
)


Next, in urls.py we'll import staticfiles_urlpatterns from django.contrib.staticfiles.urls and call that function to add the static file URL patterns to the application's URL patterns:

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
...
urlpatterns = patterns(
    # your app's url patterns here
)

urlpatterns += staticfiles_urlpatterns()

The final line there is what adds the static file URL patterns into the mix. If you output staticfiles_urlpatterns() you'll see it's something like so:

[<RegexURLPattern None ^static\/(?P<path>.*)$>]

And finally, at the very top of your templates you load the static template tags and then simply use the {% get_static_prefix %} tag to render the static URL:

{% load static %}
<html>
<head>
    <script src="{% get_static_prefix %}scripts/jquery/jquery-1.8.1.min.js"></script>
</head>
<body>
    <img src="{% get_static_prefix %}images/header.jpg" />
</body>
</html>

Conclusion

So there you have it, three approaches that more or less accomplish the same thing, but depending on the specific needs of your application or environment one approach may work better for you than another.

For our purposes on our current application we're using the first approach outlined above since it's simple and meets our needs, but it's great to know there's so much flexibility around static file handling in Django when you need it. As always read the docs for more information and yet more options for managing static files in your Django apps.

2 comments:

Christopher Clarke said...

Just remember django.views.static.serve should only be used for development

Matt Woodward said...

Thanks -- I actually hadn't read that yet, so really appreciate the tip.