Django tips: Write better template tags

Django‘s template tags are a great way to handle things that don’t always make sense being in a view. If you want to have, say, a list of recently-added content which appears in a sidebar or footer on every page of a site, it’d be crazy to manually change every view to fetch that content and add it to the template context; a template tag is definitely the way to go.

For example, in the footer of every page on this site, I pull out the five most recent entries, five most recent links and five most recent comments, and it’s all done with template tags. Now, at first it might look like there are only two ways to do this:

Write three different template tags: one for entries, one for links and one for comments. This is annoying because it’s basically repeating the same logic three times and only changing a couple of details about the model.

Write one template tag which returns three lists of content — one for each model. This is equally annoying because it lacks flexibility; I can’t pick and choose if I decide I only want certain types of recent content in a particular page’s footer, and if I ever want to add or remove types of content I have to edit both my template and my template tag code.

There is a third way, of course, and it’s both flexible and non-repetitive. We’ll get to it in a minute, but I want to walk through a few iterations of sample template tags to show everything that’s going on and point out some important concepts.

Iteration 1: Let’s fetch some links

To start with, let’s just write a template tag that fetches the five most recent links, and returns them in a context variable called recent_links. Now, most Django template tags like this come in two parts: a “compilation” function which checks the syntax you give to it, and a “renderer”, which is a Python class that gets called by the compilation function. It sounds more complicated than it actually is, so let’s look at an example. Here’s the complete code for a simple link-fetching tag:

The class LatestLinksNode is the renderer; it grabs the latest five links and shoves them into a context variable called recent_links. Like all renderers, it’s a subclass of django.template.Node — Django knows that subclasses of Node can be expected to behave in predictable ways, and will always have a method called render. The render method in this case is just being used to set a context variable, so we have it return an empty string.

The function get_latest_links is the compilation function; it doesn’t actually do much in this example except return a LatestLinksNode, but we’ll see in a moment that it can do more than that. And the rest of the code is just boilerplate which imports the functions we need, creates a new template tags library that Django will be aware of, and adds the get_latest_links function as a tag.

At this point we could do {% get_latest_links %} in a template and it’d work, but what if we want to be able to specify the number of links to get?

Iteration 2: Please, sir, I’d like some more

So what happens if we want, say, ten links instead of five? As things stand, we’d have to edit LatestLinksNode to get ten links instead of five, or maybe write a whole new template tag. But there’s a better way: we can make our template tag take an argument telling it how many links to get.

To start with, here’s how to change the compilation function to make it know to expect a numeric argument in the tag:

bits = token.contents.split() — tokens is an argument that’s given to every compilation function; if we do {% get_latest_links 5 %} in a template, the compilation function will get the string “get_latest_links 5” as its token. Here we’re splitting it into a list so we can see how many actual arguments were passed in and work with each one individually.

if len(bits) != 2: — since we want something like “get_latest_links 5”, that’ll split into a list with two items. If it doesn’t, the template tag is being called incorrectly.

return LatestLinksNode(bits[1])— lists in Python start counting at zero, not one, so if we got “get_latest_links 5” and split it, bits[1] is where the “5” will end up. We want to pass this to our renderer.

We’ve added a method called __init__, which is Python’s standard constructor for a class; notice that we’re expecting an argument to be given to it; that will be the number of links to fetch, which is now being passed by the compilation function. We want to remember that number because we’ll need it later in the render function, but if we don’t store it somewhere it’ll disappear once the __init__ method returns. So we store it in an internal variable called self.num.

And then in the render method, instead of taking five links, we just use self.num to make sure we get the right number. So now we could do {% get_latest_links 6 %} if we wanted six, {% get_latest_links 10 %} if we wanted ten, and so on.

But we’re still stuck having them end up in the template as a variable named recent_links — what if we want to be able to change that in case something else is already setting a variable with that name?

Iteration 3: Have it your way

Let’s add a little more configurability, so we can specify the name of the template context variable to store the links in; we want to be able to call the tag like this:

{%get_latest_links5asmost_recent_links%}

This means updating our compilation function again:

defget_latest_links(parser,token):bits=token.contents.split()iflen(bits)!=4:raiseTemplateSyntaxError,"get_latest_links tag takes exactly three arguments"ifbits[2]!='as':raiseTemplateSyntaxError,"second argument to the get_latest_links tag must be 'as'"returnLatestLinksNode(bits[1],bits[3])

Most of this should make sense based on what we already had: “get_latest_links 5 as most_recent_links” has four separate parts when we split it, so we check that bits has a length of 4, and we make sure the tag call passed in ‘as’ in front of the name of the variable to use to store the links. Then we pass the number of links and the variable name to the renderer.

In the __init__ method we’re getting two arguments — the number of links, and the name of a context variable to use — so we store both of them. Then in the render method, instead of setting context[‘latest_links’], we set context[self.varname].

This is fine, but what if we want to fetch something other than links, like entries or comments?

Iteration 4: Make and model

This one’s going to be a tiny bit more complicated, but should still make sense based on what we’ve been doing before. What we want now is to be able to say something like:

{%get_latestweblog.Link5asrecent_links%}

We’ll want this to go to the weblog application, get the five most recent links and store them in the context variable recent_links. We’ll rename our compilation function to match the new style (and keep in mind that the call to register it needs to change now as well), and update it to expect another argument:

defget_latest(parser,token):bits=token.contents.split()iflen(bits)!=5:raiseTemplateSyntaxError,"get_latest tag takes exactly four arguments"ifbits[3]!='as':raiseTemplateSyntaxError,"third argument to the get_latest tag must be 'as'"returnLatestContentNode(bits[1],bits[2],bits[4])

We pass the extra argument to our renderer, which we’ll now call LatestContentNode to show that it fetches any type of content. The renderer is where things get really interesting, but we’re going to have to make one small change at the top of our file before we can write it — instead of importing the Link model like we did way back in the first example, we need to do this:

Let’s look at what’s been added in the __init__ method: we’re now expecting a model to be passed in, so we want to store it. But we’re going to be getting a string that looks like “weblog.Link”; how do we translate that into an actual model class we can work with?

The answer is Django’s built-in function get_model: if you do get_model(“weblog”, “Link”), Django will look in the “weblog” application for a model named “Link”, and import it for you. So since we’re being handed something like “weblog.Link”, we split on the period and hand the result to get_model (the asterisk in there is a Python shortcut for saying “take this list of things and use it as your arguments”), then store the result in self.model.

Then in the render method we can use self.model the same way we’d use any other model class. Ordinarily we might want to use self.model.objects.all() to fetch things from the database, but Django lets you define custom managers for a model and they don’t have to be called objects. So we do self.model._default_manager.all() — which tells Django to use whatever manager is the default for that model — and fetch the appropriate number of objects. Then we store them in the correct context variable just like we did in the previous example.

And that’s it

Here’s the finished template tag, which lets us fetch any number of objects from any installed model and store them in any context variable we want:

fromdjango.templateimportLibrary,Nodefromdjango.db.modelsimportget_modelregister=Library()classLatestContentNode(Node):def__init__(self,model,num,varname):self.num,self.varname=num,varnameself.model=get_model(*model.split('.'))defrender(self,context):context[self.varname]=self.model._default_manager.all()[:self.num]return''defget_latest(parser,token):bits=token.contents.split()iflen(bits)!=5:raiseTemplateSyntaxError,"get_latest tag takes exactly four arguments"ifbits[3]!='as':raiseTemplateSyntaxError,"third argument to get_latest tag must be 'as'"returnLatestContentNode(bits[1],bits[2],bits[4])get_latest=register.tag(get_latest)

And we can call it like this:

{%get_latestweblog.Link5asrecent_links%}

Or like this:

{%get_latestweblog.Entry10aslatest_entries%}

Or like this to get comments:

{%get_latestcomments.Comment5asrecent_comments%}

Which is pretty much exactly how I generate the footer on every page of this site; I call my template tag for each type of content I want, then build up unordered lists from the content it returns. And if I ever want to change the types of content I put in the footer or the number of objects I pull out for a particular type, the only change I ever have to make is in my page template.