Writing simple templatetags is only marginally less painful that writing complex tags. There's a good deal of boilerplate code that goes into writing tags:
class SomeObjectsNode(Node):
def __init__(self, queryset, var_name, limit=5):
self.queryset = queryset
self.var_name = var_name
self.limit = int(limit)
def render(self, context):
queryset = template.Variable(self.queryset).resolve(context)
try:
objects = self.queryset[:self.limit]
except:
objects = None
context[self.var_name] = objects
return ''
@register.tag
def get_some_objects(parser, token):
"""
Get me some objects
usage:
{% get_some_objects from queryset as [var_name] (limit [limit]) %}
"""
bits = token.split_contents()
if len(bits) not in (4, 6):
raise TemplateSyntaxError('%s takes 2 or 3 arguments' % (bits[0]))
elif len(bits) == 4:
return LatestEntriesNode(bits[1], bits[3])
elif len(bits) == 6:
return LatestEntriesNode(bits[1], bits[3], bits[5])
When you get down to the core of it, though, this is what you want to express:
def get_latest_entries(context, queryset, var_name, limit=5):
try:
objects = queryset[:limit]
except:
objects = None
context[var_name] = objects
I wrote django-simpletag with the goal of making this work. There are a few quirks, but the simpletag implementation would look like this:
register = template.Library()
@simpletag(register)
def get_latest_entries(context, queryset, __as, _var_name, __limit, _limit=5):
try:
objects = queryset[:_limit]
except:
objects = None
context[_var_name] = objects
Yeah they're pretty gross, but rather than doing things in the decorator, I opted to put them right there on the function. Templatetags seem to generally take 3 kinds of arguments: string literals that matter (var_name), throwaways that should probably be validated anyways (as), and template variables that have to be evaluated. I've accomodated for these types by a convention of underscore prefixes. A single underscore, like _var_name should be evaluated as a string literal. A double-underscore, like __as, is for those throwaways that make the templatetag operations clear - it should be validated that, if present, the user entered the right thing (__as == 'as'). Lastly, no underscores means evaluate as a template variable. Default arguments can be supplied, but are limited to the same rules python uses (so no default args before required args).
It's really simple. The simpletag decorator registers a templatetag that uses your functions name. The callable passed into the templatetag registry is actually a class that wraps your function in parsing and validation logic. inspect.getargspec is used to introspect the arguments you've defined for your function. This is all of the code:
import inspect
from django import template
def simpletag(register):
def inner(func):
register.tag(func.__name__, SimpleTag(func))
return func
return inner
class SimpleTag(object):
def __init__(self, func):
self.func = func
# introspect the function, reading its arguments
args, _, _, defaults = inspect.getargspec(self.func)
assert args[0] == 'context', 'First argument of a simpletag must be "context"'
# ignore the first arg, which is always 'context'
self.args = args[1:]
if defaults:
self.num_defaults = len(defaults)
else:
self.num_defaults = 0
def __call__(self, parser, token):
bits = token.split_contents()[1:]
if len(bits) > len(self.args):
self._error('expects no more than %d arguments' % (len(self.args)))
for indx, arg in enumerate(self.args):
if arg.startswith('__'):
try:
named_bit = bits[indx]
if named_bit != arg[2:]:
self._error('expected argument %d to be "%s"' % \
(indx + 1, arg[2:]))
except IndexError:
# append an empty string to the bits
bits.append('')
if len(bits) < len(self.args) - self.num_defaults:
self._error('expects at least %d arguments' % \
(len(self.args) - self.num_defaults - 1))
return SimpleNode(self.func, bits, self.args)
def _error(self, message):
raise template.TemplateSyntaxError('%s %s' % \
(self.func.__name__, message))
class SimpleNode(template.Node):
def __init__(self, func, params, args):
self.func = func
self.params = params
self.args = args
def render(self, context):
for indx, bit in enumerate(self.params):
if not self.args[indx].startswith('_'):
self.params[indx] = template.Variable(bit).resolve(context)
return_value = self.func(context, *self.params)
return return_value or ''
django-simpletag is up on github, if you'd like to take a peek.
I recognize it's sort of a rite-of-passage to attempt to make templatetag writing less painful. There are a lot of other ways to do it, and Eric Holscher has written some great posts on the subject. Alex Gaynor has written what I think is probably the neatest solution, specifying arguments in the decorator. As always, I welcome your feedback!
My favorite is django-tagcon (http://github.com/korpios/django-tagcon). It allows a similar level of conciseness, but with a syntax that I find cleaner than either this or templatetag-sugar. Also, tags are classes so that I can reuse code via subclassing.
Carl - thanks for the link, I hadn't seen that library before nor had I considered a declarative approach. Very cool!
What do you think about putting something like kwargs in the end of the method?
def tag_name(context, **kwargs):
So you allow the use of this:
{% tag_name user=request.user %}
So while reading Pro Django I learned that Django actually has most of this functionality already. D'oh! The relevant bit is a decorator that is actually called simple_tag.
Check it out:
I'd like to humbly submit my own contribution to template tag simplification. It's called fancy_tag. It takes Django's simple_tag and adds "as <varname>" support, keyword argument support, and args/*kwargs support.
You can get it here: http://github.com/trapeze/fancy_tag/
Commenting has been closed, but please feel free to contact me