Django Cache Busting

Browsers cache images, stylesheets, javascript, it’s their default nature. They do not want to fetch the same file again and again from the server as they are smart. But sometimes, when in your app you have changes in any of the javascripts, this feature can bite you in your back. You have made some changes in your js file but it’s not reflecting in the browser. Clear the browser cache and it works. Ask your clients to do this and they will be super furious. So, what are the options here?

Browsers are forced to fetch the latest file from the server when there is a name change in the source file. Versioning the file and tagging with a new version every time a change has been done works just fine.  By adding a new version, you ask the browser to fetch the new file. Something as adding this one

?version=0.0.1 # current version

But there is one pain here – changing the version each time you have done some changes in the js file. You can easily forget to change the version and the changes in the js files do not reflect.  There are ways to accomplish this thing. I tried implementing the same taking help from one the GitHub projects.

The idea is following:

  • Write something which overwrites the existing static tag, so that you serve the files the way you are handling their names. Get an idea about custom templates and tags  from here: Custom template tags and filters
  • The render function of the custom tag class is the place where you need to add the implementation. The most general of all would be adding the current timestamp with the filename, so that whenever(if) the file is changed, it will be tagged with the timestamp of its last edit.
  • Use the custom tag instead of the default staticfiles tag.

Here goes the implementation part. The custom tag class is as below.

from django import template
from django.conf import settings

import posixpath
import datetime
import urllib
import os

try:
    from django.contrib.staticfiles import finders
except ImportError:
    finders = None

register = template.Library()


@register.tag('static')
def do_static(parser, token):
    """
    overwriting the default tag here
    """
    return CacheBusterTag(token, False)


class CacheBusterTag(template.Node):
    def __init__(self, token, is_media):
        self.is_media = is_media

        try:
            tokens = token.split_contents()
        except ValueError:
            raise template.TemplateSyntaxError, "'%r' tag must have one or two arguments" % token.contents.split()[0]

        self.path = tokens[1]
        self.force_timestamp = len(tokens) == 3 and tokens[2] or False

    def render(self, context):
        """
        rendering the url with modification
        """
        try:
            path = template.Variable(self.path).resolve(context)
        except template.VariableDoesNotExist:
            path = self.path

        path = posixpath.normpath(urllib.unquote(path)).lstrip('/')
        url_prepend = getattr(settings, "STATIC_URL", settings.MEDIA_URL)

        if settings.DEBUG and finders:
            absolute_path = finders.find(path)
        else:
            absolute_path = os.path.join(getattr(settings, 'STATIC_ROOT', settings.MEDIA_ROOT), path)

        unique_string = self.get_file_modified(absolute_path)
        return url_prepend + path + '?' + unique_string

    @staticmethod
    def get_file_modified(path):
        """
        get the last modified time of the file
        """
        try:
            return datetime.datetime.fromtimestamp(os.path.getmtime(os.path.abspath(path))).strftime('%S%M%H%d%m%y')
        except Exception as e:
            return '000000000000'

Now comes the part where we serve the view.

from django.http import Http404
from django.contrib.staticfiles.views import serve as django_staticfiles_serve

"""
Views and functions for serving static files.
"""

def static_serve(request, path, document_root=None):
    try:
        return django_staticfiles_serve(request, path, document_root)
    except Http404:
        unique_string, new_path = path.split("/", 1)
        return django_staticfiles_serve(request, new_path, document_root)

In the base URLs class where you call the  Django’s default serve method, change that one to the following:

urlpatterns = patterns('',
                       url(r'^static/(?P.*)$', 'cachebuster.views.static_serve', {'document_root': STATIC_URL}),
                       .....
)

Now call the custom tag to load the static files.

{% load cachebuster %}

That’s it. This works for me. So, cheers.

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