Tech Blog

Django Template Tags

Django Template Tags

Recently updated on

It seems like I frequently find myself needing to write template tags for the various projects I work on here at Imaginary Landscape. The most common reason is generally so that I can adhere to the ideas of the DRY principle. For instance, YaBa has the potential to craft a rather lengthy sidebar with Twitter updates, GitHub activity, links, categories and archives, all of which are dynamic content. To make it more problematic, that side bar is in every single view. In order to accomplish that without template tags, I'd need to return objects for each of those in every single view that displays the side bar. Needless to say, that's a lot of unnecessary code and a waste of time. Hence, I wrote a simple navigation tag which leverages Django's inclusion tags. I find these to be the simplest tags to write, but also very limited in the sense of features and extensibility. They allow us to return variables to the context and render it out to a template, that template then gets included whenever we use the template tag.

This worked pretty well for YaBa, since I was creating a side bar. If I was creating a more traditional tag, I would have still created a specific template for the side bar and then included that template into other templates - same basic effect in a nutshell, really. Unfortunately, not everything is as straightforward and simple as the sidebar scenario in YaBa. While we were creating ChicagoDjango.com with Django-CMS, we found that our front page needed headlines from our news application, headlines for our blogging application, and a random photo from a random gallery (we used django-photologue for our gallery needs). Writing three different inclusion tags or general template tags sounded rather tedious and like a pretty severe DRY violation to boot. That's why I came up with a different idea.

I figured, why not create a template tag that will let me grab any number of objects, from any model, in any sort, with a variable name of my choosing? My hunch was that something such as that would be incredibly beneficial to us, since most of the template tags I write are for getting data from a particular model to a multitude of different views. Below is the first part I came up with:

from django import template

from django.db.models import get_model

register = template.Library()

class ModelObjectNode(template.Node):
    def __init__(self, app_name, model_name, sort, count, var_name):
        self.app_name = app_name
        self.model_name = model_name
        self.sort = sort
        self.count = int(count)
        self.var_name = var_name

    def render(self, context):
        try:
            model = get_model(self.app_name, self.model_name)
        except:
            raise TemplateSyntaxError, "Failed to retrieve model"

        object_list = model.objects.all()
        object_list = object_list.order_by(self.sort)
        if object_list.count() > self.count:
            object_list = object_list[:self.count]
        context.update({self.var_name: object_list})
        return ''

We first import template and then the key of this operation, django.db.models.get_model, which allows you to pass strings to it to grab a model from a Django application on your Python Path. The class, "ModelObjectNode", is a template node that will be used to update the context. We have a simple init function to map variables as they're passed in and, as you can see, this uses five variables. It is a bit lengthy, but they're fairly self-explanatory:

app_name = Name of the application from which we'll be importing

model_name = Name of the map from the aforementioned application

sort = How you'd like to sort the queryset

count = How many objects you want

var_name = The name of the variable you'll use in the template

In the render function we attempt to get the model. If it fails, we throw a TemplateSyntaxError to let the user know we failed to retrieve a model. Assuming it passes that step, we grab our list of objects, sort it and then do a comparison between the length of the queryset and the count variable. If the queryset is too long we slice it, if not, we leave it alone. Then we update the context. Next we have the actual function that is called by the template tag:

def get_object_models(parser, token):
    try:
        tag_name, app_name, model_name, sort, count, var_name = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError("Object Tag requires 5 variables")

    return ModelObjectNode(app_name[1:-1], model_name[1:-1], 
        sort[1:-1], count[1:-1], var_name[1:-1])

We start off with the @register.tag decorator to register "get_object_models" as a template tag. Now skip down to the try block, this is where we take all of the template tag variables in token, split it out and dump the values into variables. If that fails, it means that an improper number of variables has been passed to us and we can't properly handle the input as a result. We then return a ModelObjectNode initialized with these variables. You'll notice a [1:-1] at the end of each variable as well. That's because, annoyingly, each variable will be surrounded by quotes and this is a simple way of clearing them out. You can use .strip('"'), but that's more typing than I care for.

Now when it comes time to use the template tag, all you need to do is:


 

{% load get_objects %}

{% get_object_models "django_yaba" "Story" "-created" "3" "blog_posts" %}
{% if blog_posts %}

{% for post in blog_posts %}
{{ post.title }}
{% endfor %}
{% endif %}

This will grab the Story model from YaBa, sort them by the latest created entries and slice it down to just 3 entries, all with a variable name of 'blog_posts'. Then I can iterate through those and create a list of recent posts on the front page. It's pretty easy and straightforward, as you can see.

Comments

Comments are closed.