Skip to main content

Filtering and Searching Data

The filtering system in the Django admin provides a sidebar for the change list view, allowing users to drill down into data based on specific field values or custom logic. This system is primarily driven by the list_filter attribute in a ModelAdmin class and is implemented through a hierarchy of filter classes in django/contrib/admin/filters.py.

The Filter Hierarchy

All admin filters inherit from the base ListFilter class. This class defines the interface required for a filter to appear in the sidebar and modify the resulting queryset.

  • ListFilter: The abstract base class. It requires subclasses to implement has_output(), choices(), queryset(), and expected_parameters().
  • FieldListFilter: A subclass designed for filters that map directly to model fields. It includes a registration mechanism to automatically select the appropriate filter type (e.g., BooleanFieldListFilter for boolean fields).
  • SimpleListFilter: A high-level class for creating custom filters that are not necessarily tied to a single field or require complex logic.

Custom Filtering with SimpleListFilter

When you need to filter data based on logic that doesn't map directly to a field lookup, you implement a subclass of SimpleListFilter. This class simplifies the process by handling the query string parameter management.

To implement a SimpleListFilter, you must define:

  1. title: The human-readable heading for the filter in the sidebar.
  2. parameter_name: The URL query string parameter used for this filter.
  3. lookups(): Returns a list of tuples (value, verbose_name) representing the choices in the sidebar.
  4. queryset(): Applies the filtering logic to the provided queryset based on the selected value.

Example: Filtering by Decade

As seen in tests/admin_filters/tests.py, a custom filter can be used to group records into logical buckets like decades:

from django.contrib.admin import SimpleListFilter

class DecadeListFilter(SimpleListFilter):
title = "publication decade"
parameter_name = "publication-decade"

def lookups(self, request, model_admin):
return (
("the 80s", "the 1980's"),
("the 90s", "the 1990's"),
("the 00s", "the 2000's"),
)

def queryset(self, request, queryset):
# self.value() returns the value from the query string
decade = self.value()
if decade == "the 80s":
return queryset.filter(year__gte=1980, year__lte=1989)
if decade == "the 90s":
return queryset.filter(year__gte=1990, year__lte=1999)
if decade == "the 00s":
return queryset.filter(year__gte=2000, year__lte=2009)

Field-Based Filtering

FieldListFilter and its subclasses handle standard field types. When you add a field name to ModelAdmin.list_filter, Django uses FieldListFilter.create() to instantiate the correct filter class based on the field's type.

Automatic Mapping

The system uses a registration pattern defined in django/contrib/admin/filters.py. For example:

  • BooleanField uses BooleanFieldListFilter.
  • DateField uses DateFieldListFilter (providing options like "Today", "Past 7 days", etc.).
  • Fields with choices defined use ChoicesFieldListFilter.
  • Foreign keys use RelatedFieldListFilter.

Specialized Field Filters

You can explicitly specify a filter class for a field in list_filter using a tuple. A common example found in tests/admin_filters/tests.py is using RelatedOnlyFieldListFilter to limit the filter options to related objects that actually exist in the current queryset:

class BookAdmin(ModelAdmin):
list_filter = (
"year",
("author", RelatedOnlyFieldListFilter),
)

Facets and Counts

The FacetsMixin (mixed into both SimpleListFilter and FieldListFilter) enables the "facets" feature, which displays the count of records for each filter option in the sidebar (e.g., "the 80s (12)").

The counts are calculated in get_facet_counts(). For SimpleListFilter, this method iterates through lookup_choices and performs an aggregate count on the queryset:

# From django/contrib/admin/filters.py
def get_facet_counts(self, pk_attname, filtered_qs):
original_value = self.used_parameters.get(self.parameter_name)
counts = {}
for i, choice in enumerate(self.lookup_choices):
self.used_parameters[self.parameter_name] = choice[0]
lookup_qs = self.queryset(self.request, filtered_qs)
if lookup_qs is not None:
counts[f"{i}__c"] = models.Count(
pk_attname,
filter=models.Q(pk__in=lookup_qs),
)
self.used_parameters[self.parameter_name] = original_value
return counts

This feature is controlled by the add_facets attribute on the ChangeList view, which determines if the choices() method should include count information in its output.

Integration with ModelAdmin

The ModelAdmin class processes the list_filter attribute during the initialization of the ChangeList. Each entry in list_filter results in a filter instance that is stored in changelist.filter_specs.

When the ChangeList generates the final queryset, it iterates through these filters and calls their queryset() methods sequentially, passing the result of the previous filter to the next. This allows multiple sidebar filters to work together to narrow down the data.