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 inlist_filtersadds 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_citationand 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 meansself.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.