Getting the most out of Django Admin filters

Chesco Igual
Elements blog
Published in
6 min readMar 16, 2015

An often forgotten part of a project, the administration site (or backend) usually does not get the attention it deserves, as it is the “less visible” part of our website or web application. But specially when this part of the site is going to be used by someone other than you, having an intuitive flow, good filters or a good-looking theme and menu, may be big factors that help you achieve excellence in your products.

In the following article we will describe a couple of filtering tricks that may be the icing on the cake for your admin site experience in Django (version 1.7 at the moment of writing this article).

The Basics

Let’s put ourselves in context and start from the most simple filtering tools that Django provides us. Django allows the user of the admin site to filter the instances of a Model by adding the list_filter attribute to your ModelAdmin objects. You can find more information about the Django’s filtering utilities in the official documentation, in the Django Admin Site section.

In summary, you can overwrite the default filtering by creating a subclass of django.contrib.admin.SimpleListFilter and overriding the lookups and queryset methods. Nevertheless we won’t repeat here the class’ basic usage here, so please check out Django’s documentation to get to know the class a little bit more.

Once the basics are covered, we can get to the main point of this article, which is learning some advanced usages of the Django filtering system. We will cover two cases: adding an automatic default filtering so the model admin always displays a subset of the whole amount of records, and adding two filters which are related to each other.

The data we will use in this tutorial is a simple Django app for keeping track of the users’ Pets containing three Models:

  • Species: Dog, Cat, Turtle…
  • Breed: German Shepherd, Siamese, …
  • Pet: Tobby, Scooby Doo, Felix, …

All the code used in this article can be found in the following repository: https://github.com/chescales/adminfilters.

Adding an automatic default list_filter to a Model

Let’s say that for our Breed’s model admin, we do not want the user to be able to see all of the different Species’ Breeds at the same time, so dogs at one point, or cats, or any other, but not all of them together.

In order to do that, we have to override an additional method of the SimpleListFilter class, the method value. This method is the responsible to tell the filter if there is a selected item to filter by or not. To achieve this, we have added the value method to our filter:

from django.contrib import admin
from adminfilters.models import Species, Breed

class SpeciesListFilter(admin.SimpleListFilter):

"""
This filter will always return a subset of the instances in a Model, either filtering by the
user choice or by a default value.
"""
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = 'species'

# Parameter for the filter that will be used in the URL query.
parameter_name = 'species'

default_value = None

def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
list_of_species = []
queryset = Species.objects.all()
for species in queryset:
list_of_species.append(
(str(species.id), species.name)
)
return sorted(list_of_species, key=lambda tp: tp[1])

def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
# Compare the requested value to decide how to filter the queryset.
if self.value():
return queryset.filter(species_id=self.value())
return queryset

def value(self):
"""
Overriding this method will allow us to always have a default value.
"""
value = super(SpeciesListFilter, self).value()
if value is None:
if self.default_value is None:
# If there is at least one Species, return the first by name. Otherwise, None.
first_species = Species.objects.order_by('name').first()
value = None if first_species is None else first_species.id
self.default_value = value
else:
value = self.default_value
return str(value)

@admin.register(Breed)
class BreedAdmin(admin.ModelAdmin):
list_display = ('name', 'species', )
list_filter = (SpeciesListFilter, )

Just overriding the value method, what we achieve is that if no filter is selected, we give the filter a default value. The rest of the Filter will already return the filtered query set to the ModelAdmin.

Here are the results. Before the updated filter, accessing to the Breed admin would return all of the records in the database:

However, after applying the new filter, accessing the default list will already return a filtered list by a default value (“Domestic Cat” in our case):

Even hitting the ‘All’ filter will return the list filtered by the default value.

Adding two filters to a Model related to each other

Let’s say we want to allow the users to filter at two different levels of Foreign Keys, and that filtering from one of the filters should alter the amount of possible options in the other filter. In our example, we may want to allow the admin site users to filter their Pets by either their Species, or by their Breed, but if a Species is selected, only the Breeds associated to it should be displayed as available filters.

In order to achieve this connection between filters, we will again subclass SimpleListFilter class, but this time we will only need to use an updated version of the lookups method. In this method, we will check the request for any existing filters of the other list_filter (Species), and filter the possible options for the Breeds depending on this possible filter:

from django.contrib import admin
from adminfilters.models import Breed, Pet

class BreedListFilter(admin.SimpleListFilter):
"""
This filter is an example of how to combine two different Filters to work together.
"""
# Human-readable title which will be displayed in the right admin sidebar just above the filter
# options.
title = 'breed'

# Parameter for the filter that will be used in the URL query.
parameter_name = 'breed'

# Custom attributes
related_filter_parameter = 'breed__species__id__exact'

def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
list_of_questions = []
queryset = Breed.objects.order_by('species_id')
if self.related_filter_parameter in request.GET:
queryset = queryset.filter(species_id=request.GET[self.related_filter_parameter])
for breed in queryset:
list_of_questions.append(
(str(breed.id), breed.name)
)
return sorted(list_of_questions, key=lambda tp: tp[1])

def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
# Compare the requested value to decide how to filter the queryset.
if self.value():
return queryset.filter(breed_id=self.value())
return queryset


@admin.register(Pet)
class PetAdmin(admin.ModelAdmin):
list_display = ('name', 'get_species', 'breed', 'birth', )
list_filter = ('breed__species', BreedListFilter)

Check out the behaviour of this filter in the following screenshots. This is how the unrelated filters worked if just filtering separately by Species and Breed, after filtering the Pets admin by the value “Domestic Dog”.

You can see that all the “Domestic Cat” Breeds are still showing up in the Breeds filter on the right. However, if we activate our custom BreedListFilter, the possible options of the Breed filter are connected to the other list_filter for Species:

And voilà! With these little Django Admin tricks you will improve the User Experience of your back-end site, and really give an extra level in quality for your customers and users. They will for sure be grateful for your effort on it.

Don’t forget to follow us on Facebook and Twitter!

Originally published at www.elements.nl on March 16, 2015.

--

--

Chesco Igual
Elements blog

Sports lover, I also greatly enjoy travelling. Senior backend Django, also really enjoy devops & Ansible. Discworld citizen.