The Problem
Giving users a view into a long list of items is a common function. A popular way to manage that view is to use Django's built in pagination feature, allowing links to different pages to be easily created.
This works well as long as no other GET parameters need to be used in parallel. It starts to break down a bit as sorting, filtering, page size, and other functions are added. This post will annotate and explain an elegant fix found at https://djangosnippets.org/snippets/1627/
A Little More In Depth
To be clear, the above problem is not insurmountable. Assuming a Django template with a context containing:
- a
page
object named
page_obj
(paired with the GET parameter ofpage
) - a filter constraint
filter
(paired with the GET parameter offilter
) - a sort constraint
sort
(paired with the GET parameter ofsort
)
then:
<a href="?page={{ page_obj.next_page_number }}&filter={{ filter }}&sort={{ sort }}">
next
</a>
will work. However, it requires all of the GET parameters to be known, as well as their values. It also would need to be updated any time the possible GET parameters change. This is a lot of extra work for something that is tangential to the pagination function.
The Setup
To lessen the technical burden of pagination, implement a template
tag. The
first thing to do when creating a custom template tag is to decide which app to
put it in. If the tag is only needed for a single app, then it can be put
inside that app's directory. It can also be placed in a dedicated app. The only
requirement in order to use the tag is that the app it is defined in is
included in the INSTALLED_APPS
config tuple.
Create a directory named templatetags
inside of the chosen app, create
the __init__.py
file, and then start a new file named
foo_extras.py
, where foo
is the app name. Start the template tag
off with:
from django import template
register = template.Library()
This sets up the module to implement a Django template tag.
A Helping Hand
Template tags generally require a bit of boilerplate that does not change much across all tags. Make a decorator that reduces the need to worry about this boilerplate:
def easy_tag(func):
def inner(parser, token):
try:
return func(*token.split_contents())
except TypeError:
raise template.TemplateSyntaxError(
'Bad arguments for tag "%s"' % token.split_contents()[0])
inner.__name__ = func.__name__
inner.__doc__ = func.__doc__
return inner
The easy_tag
decorator wraps the real logic of the tag with the necessary
parsing of the tag parameters. The inner
function has the signature
required of a template tag: the two arguments parser
and token
.
parser
will not be used in this tag, so it can be ignored. token
is
the tag in its entirety, as it exists in the template. The inner function tries
to call the split_contents
method of the token, which splits the text of
the entire tag into space delimited pieces. Note that split_contents
is
used, which takes care to keep quoted strings as a single item (as opposed to
split
which does not).
For example, if the template were to call a test tag:
{% test foo "bar baz" foo %}
And a template tag was defined as:
@register.tag()
@easy_tag
def test(tag_name, arg1, arg2, arg3):
...
Then the test
function would be wrapped in the easy_tag call. The
easy_tag decorator would handle splitting the args into the pieces
['test', 'foo', '"bar baz"', 'foo']
and passing that as the arg list into test
. In this case, tag_name would
be assigned the value test
, since that is the first item in the list.
arg1
would be assigned foo
, arg2
would be assigned "bar
baz"
, and arg3
would be assigned foo
. In addition to splitting
the tag, the decorator handles catching any errors encountered while executing
the tag's code, and throwing a TemplateSyntaxError
exception. It creates
a descriptive exception message by adding in the tag name, which is always the
first item of the list returned by split_contents
. It also sets the name
and documentation string of the function being wrapped to the new function
created by the decorator in order to keep everything consistent.
Parsing the Tag Arguments
The next step in creating the tag is to begin implementing the
template.Node
subclass. This will be the meat of the custom tag. A Node
object is what must be be returned when registering a template tag via
register.tag
. The Node object requires two methods: __init__
and
render
. The first step will be to create the __init__
method:
class AppendGetNode(template.Node):
def __init__(self, args):
self.dict_pairs = {}
for pair in dict.split(','):
pair = pair.split('=')
self.dict_pairs[pair[0]] = template.Variable(pair[1])
The __init__
method should handle compiling the tag argument into a
usable data structure. The method will assume that argument is a string whose
value is a comma-delimited list of key value pairs, and that each key value
pair will be separated by an equals sign. It also assumes that each value is a
template variable, and must be resolved. It uses each key and resolved value to
construct a dictionary of data, saving it as the instance variable
dict_pairs
. These will be used as the first step when creating the new
request.
The Render Method
The render
method is the other required method of the Node
instance.
It will handle the final creation of the string that will be returned as the
output of the tag.
def render(self, context):
get = context['request'].GET.copy()
for key in self.dict_pairs:
get[key] = self.dict_pairs[key].resolve(context)
path = context['request'].META['PATH_INFO']
if len(get):
path += "?%s" % "&".join(
["%s=%s" % (key, value) for (key, value) in get.items() if value])
return path
The first thing the render method does is create a copy of the current GET
QueryDict.
A copy is made because the GET object itself is immutable. Using this as a
starting point, the key/value pairs passed in via the tag are added
(overwriting any of the current GET parameters). The path is retrieved from the
request headers, and then the new GET QueryDict
is checked. If it has
any items, they are appended to the path to return the new request.
Final Touches
The final step to implement the template tag is to create a function that the decorators can be applied to. This will be the entry point of the tag.
@register.tag()
@easy_tag
def append_to_get(tag_name, args):
return AppendGetNode(dict)
A function named append_to_get
is created, which takes in the name of the
tag as tag_name
, and the single argument of the tag (the comma-delimited
key-value pairs) as args
. This function is first decorated by the
easy_tag
decorator. As described previously, this decorator:
- accepts a function as an argument
- returns a function that:
- has the arguments expected of a template tag (taking in the
parser
andtoken
args) - returns the result of the function it decorates (
append_to_get
) after it has been passed the result ofsplit_contents
- has the arguments expected of a template tag (taking in the
That function is then decorated again with the standard Django
register.tag
decorator, in order to ready it for use in a Django app.
The append_to_get
method itself merely creates the template.Node
subclass.
The Final Product
Putting everything together, this is what the final product looks like:
from django import template
register = template.Library()
"""
Decorator to facilitate template tag creation
"""
def easy_tag(func):
"""Split the tag into pieces and call the wrapped function with the
list that is returned."""
def inner(parser, token):
try:
return func(*token.split_contents())
except TypeError:
raise template.TemplateSyntaxError(
'Bad arguments for tag "%s"' % token.split_contents()[0])
inner.__name__ = func.__name__
inner.__doc__ = func.__doc__
return inner
class AppendGetNode(template.Node):
def __init__(self, args):
self.dict_pairs = {}
for pair in args.split(','):
pair = pair.split('=')
self.dict_pairs[pair[0]] = template.Variable(pair[1])
def render(self, context):
get = context['request'].GET.copy()
for key in self.dict_pairs:
get[key] = self.dict_pairs[key].resolve(context)
path = context['request'].META['PATH_INFO']
if len(get):
path += "?%s" % "&".join(
["%s=%s" % (key, value) for (key, value) in get.items() if value])
return path
@register.tag()
@easy_tag
def append_to_get(tag_name, args):
return AppendGetNode(dict)
New and Improved
With all of the above in place, the repetitive and brittle template from above:
<a href="?page={{ page_obj.next_page_number }}&filter={{ filter }}&sort={{ sort }}">
next
</a>
can now be replaced with:
{% load foo_extras %}
<a href="{% append_to_get page=page_obj.next_page_number %}">
next
</a>
No GET parameters need be worried about, other than the page parameter. The template also will handle any new GET parameters by default, as it will submit the existing request's parameters without any extra instruction!.