January 20, 2009
by Alex Lemann
0 comments
Categories:
Technical
Tags:
django

Overriding Django admin templates for fun and profit

Motivation & Goal

I sometimes find the admin interface's lists of instances of models overwhelming. The filters and searching helps, but it would be nice to get an overview of the data that is being shown. Particularly I wanted to generate a graph based on the filters selected by the user, so that only items displayed after a filter would be graphed.

For example, if you have a Post model in your Blog application and a filter by author, this code might graph the number of Posts per day of the week to get a sense of when to release your next Post. But, if you select an author from the filter you might want to just graph the number of Posts created only by that one author on each day of the week.

Django has amazing documentation. There is an example in the docs describing how to override the change_form.html template, but no example overriding the change_list.html file. The change_list.html template is the template that describes the list of a particular model's objects in the admin interface. And, there is especially no example that uses the selected filters to change the content of the list of objects in the admin interface.

Process

Following the documentation above, I overrode the templates/admin/my_app/my_model/change_list.html file. This means that my changes to the templates will only show up on that particular object's change_list page. We want to show the graphs above the list of objects only for this one model. Examining the original change_list.html file, it turns out that the header for this list of objects corresponds to the pretitle block. The fact that it is so simple to override a single block in one template for one model in the admin interface speaks to how modular Django is. What do we want to put in that block? The filter data is buried deep within the context passed in to this page from a view. We certainly don't want to mess with the admin's views. That leaves template tags. Now, template tags are usually a last resort for me because of the unwieldy argument passing syntax. Caktus has our own internal way of doing this which is extremely similar to a django snippet posted recently. So, the template should look like:

{% extends "admin/change_list.html" %}
{% load graphs_from_filters %}

{% block pretitle %}
{% graphs_from_filter change_list=cl %}
{% endblock %}

Notice, we are passing the ChangeList variable, cl, from the context into our template tag.

Digging into a ChangeList

So, we've been passed django.contrib.admin.main.views.ChangeList object. What the heck is that? Well change_list objects hold django.contrib.admin.filter_spec.FilterSpecs objects which give us the name of the model, the class, and the id of the particular object. We use that to create a dictionary of filter name to filtered by objects. It looks something like this:

from django import template
from caktus.django.templatetags import parse_args_kwargs, CaktNode
import project.graphs as graphs

class GraphFilterNode(CaktNode):
    def render_with_args(self, context, change_list):
        selected = {}
        for filter_spec in change_list.filter_specs:
          if filter_spec.lookup_val:
            selected[filter_spec.title()] = filter_spec.field.rel.to.objects.get(id=filter_spec.lookup_val)
    return "<img src="%s" />" % graphs.graph_url_from_filters(selected)

register = template.Library()
@register.tag
def graphs_from_filter(parser, token):
  """
    Usage {% graphs_from_filter change_list=<change_list_object> %}
    """
    tag_name, args, kwargs = parse_args_kwargs(parser, token)
    return GraphFilterNode(*args, **kwargs)</change_list_object>

Disclaimer

I know very little about Django internals. This was mostly worked out through ipython, introspection, and reading some code. There is a little bit of validation of this method in Django ticket #3096, but as usual the internal Django structures might change and break your code. This happend to me when this ticket got resolved. I think that now I have this, a better solution, but is it the best one?

blog comments powered by Disqus