Custom admin filters in Django

Django’s admin interface is pretty much the best thing ever. It’s one of Django’s biggest selling points, and for good reason — it’s a blessing for developers and, erm, non-developers alike. I work part-time at William and Mary’s AidData branch, and our backend runs on Django. It’s perfect — gives our non-techy researchers enough flexibility and access to the data without dirtying their hands with SQL, and gives me and the other developers a great frontend to do spot updates and easy cascading.

The admin interface is incredibly flexible, too — you can pretty much customize any aspect of it. For instance, take these filters (basically clickable WHERE clauses):

These are auto-generated by the Admin class for the given table you’re accessing, usually by just giving it a tuple of an attribute for each filter. So if we were a nationwide used car agency, a filter on a database of cars is as easy as:

list_filter = [‘state’, ‘make’, ‘model’]

Tada! Suddenly we have three filters, autopopulated with each possible state, make, or model in our database.

Creating custom filters

These filters, unfortunately, are verbose by default. The issue I ran in today came from the following email:

We’re tracking all citations in foreign aid journalism in trip_citation. I’d love to be able to filter this on each publication being cited, but adding this in list_filters adds a list of twelve thousand books… when right now there are only 25 that have been cited. Any ideas?

The issue was that the field being filtered was a foreign key to a massive table, and filtering on that foreign key meant Django was grabbing all of the entries in that foreign key’s table instead of only the ones being referenced in trip_citation.

Thankfully, the solution — posted below — is pretty simple:

class CitedBooksFilter(SimpleListFilter):
  title = _('cited book')
  parameter_name = 'book_title'

  def lookups(self, request, model_admin):
    cited_books =  set([c.cited_book for c in Citation.objects.all()])
    return [(c.id, c.title) for c in cited_books]

  def queryset(self, request, queryset):
    if self.value():
      return queryset.filter(cited_book__id__exact=self.value())
    else:
      return queryset

Django supplies a SimpleListFilter parent class — basically how all of the default filters work — and it’s up to us to overwrite four specific things:

  • title — exactly what it sounds like. How you’d describe the filter.
  • parameter_name — basically a less verbose title. This is what goes in the URL when you apply the filter.
  • lookups — All of the possible values for the filter. In my above case, I iterated over trip_citation and grabbed the distinct foreign key elements, thus giving me only the publications which were being cited.
  • queryset — The other side of lookups: how to filter a given queryset if the filter is being used. In the above case, it’s pretty simple: make sure the foreign key is equal. (Note: surrounding this with an if/else clause is incredibly important. All filters are evaluated: if an option isn’t clicked, that just means self.value() == 0. If you don’t surround this with an if/else clause, you’ll be searching for entries that have a foreign key of zero, which would never end well.

And invoking the filter is crazy simple:

list_filter = (CitedBooksFilter)

Hope this helped! If you like programming in Django, you should follow me on Twitter.