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.