How to Add a Text Filter to Django Admin

How we made Django admin faster by adding a new type of filter

When creating a new Django Admin page a common conversation between the developer and the support personal might sound like this:

search_fields = (
user__username,
)
search_fields = (
user__username,
user__email,
)
search_fields = (
user__username,
user__email,
user__first_name,
user__last_name,
)
search_fields = (
user__username,
user__email,
user__first_name,
user__last_name,
payment__voucher_number,
)
search_fields = (
user__username,
user__email,
user__first_name,
user__last_name,
payment__voucher_number,
invoice__invoice_number,
)
search_fields = (
user__username,
user__email,
user__first_name,
user__last_name,
payment__voucher_number,
invoice__invoice_number,
uid,
user__uid,
payment__uid,
invoice__uid,
)

The Problem With Search Fields

Django Admin search fields are great, throw a bunch of fields in search_fields and Django will handle the rest.

Bridging the gap between Django and the user

We started thinking of ways we can create multiple search fields — one for each field or group of fields. We thought that if the user want to search by email or UID there is no reason to search by any other field.

  • ListFilter can have a custom template.
  • Django already has support for multiple ListFilters.

Implementing InputFilter

What we want to do is have a ListFilter with a text input instead of choices.

class UIDFilter(InputFilter):
parameter_name = 'uid'
title = _('UID')
def queryset(self, request, queryset):
if self.value() is not None:
uid = self.value()
return queryset.filter(
Q(uid=uid) |
Q(payment__uid=uid) |
Q(user__uid=uid)
)
class TransactionAdmin(admin.ModelAdmin):    # ...    list_filter = (
UUIDFilter,
)
# ...
  • We set the parameter_name in the URL to be uid. A URL filtered by uid will look like this /admin/app/transaction?uid=<uid>
  • If the user entered a uid we search by transaction uid, payment uid or user uid.
class InputFilter(admin.SimpleListFilter):
template = 'admin/input_filter.html'
def lookups(self, request, model_admin):
# Dummy, required to show the filter.
return ((),)
<!-- templates/admin/input_filter.html -->{% load i18n %}<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
<li>
<form method="GET" action="">
<input
type="text"
value="{{ spec.value|default_if_none:'' }}"
name="{{ spec.parameter_name }}"/>
</form>
</li>
</ul>

Play Nice With Other Filters

So far our filter works but only if there are no other filters. If we want to play nice with other filters we need to consider them in our form. To do that, we need to get their values.

class InputFilter(admin.SimpleListFilter):
template = 'admin/input_filter.html'
def lookups(self, request, model_admin):
# Dummy, required to show the filter.
return ((),)
def choices(self, changelist):
# Grab only the "all" option.
all_choice = next(super().choices(changelist))
all_choice['query_parts'] = (
(k, v)
for k, v in changelist.get_filters_params().items()
if k != self.parameter_name
)
yield all_choice
<!-- templates/admin/input_filter.html -->{% load i18n %}<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
<ul>
<li>
{% with choices.0 as all_choice %}
<form method="GET" action="">
{% for k, v in all_choice.query_parts %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
<input
type="text"
value="{{ spec.value|default_if_none:'' }}"
name="{{ spec.parameter_name }}"/>
</form>
{% endwith %}
</li>
</ul>
<!-- templates/admin/input_filter.html -->...<input
type="text"
value="{{ spec.value|default_if_none:'' }}"
name="{{ spec.parameter_name }}"/>
{% if not all_choice.selected %}
<strong><a href="{{ all_choice.query_string }}">⨉ {% trans 'Remove' %}</a></strong>
{% endif %}
...

Bonus

Search Multiple Words Similar to Django Search

You might have noticed that when searching multiple words Django find results that include at least one of the words and not all.

from django.db.models import Qclass UserFilter(InputFilter):
parameter_name = 'user'
title = _('User')
def queryset(self, request, queryset):
term = self.value()
if term is None:
return
any_name = Q()
for bit in term.split():
any_name &= (
Q(user__first_name__icontains=bit) |
Q(user__last_name__icontains=bit)
)
return queryset.filter(any_name)

I’m Nima , CTO at Atrotech based in Tehran. Django developer and enthusiastic in CV, IOT, AI and data-driven technologies.