Skip to main content

django-admin-select-filter

Select2-powered admin list filters for Django.

Select2 filters in the admin sidebar

Install

pip install django-admin-select-filter

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "django_admin_select_filter",
]

Usage

ChoiceFilter

The main filter: it works on any field with discrete values — a choices-backed CharField/IntegerField, a BooleanField, or an explicit options list you provide yourself:

from django.contrib import admin
from django_admin_select_filter import ChoiceFilter

from myapp.models import Book


class GenreFilter(ChoiceFilter):
    parameter_name = "genre"  # reads Book.genre.choices


class StatusFilter(ChoiceFilter):
    parameter_name = "status"
    options = [("draft", "Draft"), ("published", "Published")]


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_filter = [GenreFilter, StatusFilter]

ChoiceFilter dropdown open, listing Fiction/Non-fiction

Since options accepts any explicit (value, label) list, ChoiceFilter can even filter a ForeignKey — pass one pair per related row:

class AuthorChoiceFilter(ChoiceFilter):
    parameter_name = "author"
    options = [(author.pk, str(author)) for author in Author.objects.all()]

This isn't optimal, though: options is evaluated once, when the class body runs (at import time), so it goes stale as rows are added or renamed — there's no live query, search, or async loading behind it. For a real ForeignKey, use ForeignKeyFilter below instead.

ForeignKeyFilter

Built specifically for ForeignKey/ManyToManyFields: it queries the related model live, supports searching through the related model's own ModelAdmin.search_fields, and can defer loading options until the widget is opened (see async_call further down).

from django_admin_select_filter import ForeignKeyFilter


class AuthorFilter(ForeignKeyFilter):
    parameter_name = "author"
    ordering = ["name"]


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_filter = [AuthorFilter]

ForeignKeyFilter dropdown with a search box, listing authors

model is only needed when it can't be inferred. By default it's resolved by walking parameter_name across Book's relations, following a nested lookup (e.g. parameter_name = "author__country") segment by segment — forward or reverse — and taking the last segment's related model. Set model explicitly if the lookup isn't a real relation chain, or to point somewhere else.

field_list_filter()

For a one-off filter that doesn't need its own subclass, use Django's own list_filter shorthand — a (field_name, filter_class) tuple — via field_list_filter():

from django_admin_select_filter import ChoiceFilter, ForeignKeyFilter, field_list_filter


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_filter = [
        ("author", field_list_filter(ForeignKeyFilter)),
        ("genre", field_list_filter(ChoiceFilter)),
    ]

parameter_name (and, for ForeignKeyFilter, model) is inferred from the field automatically — including a nested lookup like "author__country" — so the same wrapped class can be reused across as many fields as you like. It only supports async_call = False; for an async_call filter, define a dedicated subclass instead (field_list_filter() raises TypeError right away if you pass it one, rather than fail silently later).

Shared options

Both filters share the same options: filter_only_used_values, async_call, searchable, nullable and title. Both also support a nested lookup for parameter_name (e.g. "author__status"), resolving the field — and, for ForeignKeyFilter, the model — by walking each __-separated relation in turn, forward or reverse.

Set searchable = False to hide the search input entirely and get a plain dropdown instead — best for a short, static option list.

Set multiple = True to let either filter accept several values at once:

class AuthorFilter(ForeignKeyFilter):
    parameter_name = "author"
    multiple = True

multiple = True with two selected author chips

Selected values are joined in the query string with multiple_separator ("," by default) and applied with an __in lookup, so configured values (primary keys, option values) must not contain that character. The "All" option is dropped in this mode — clearing every selected chip already means no filter.

Async options (async_call = True)

Set async_call = True on a filter to defer loading its options until the Select2 widget is opened, fetching them over AJAX instead of rendering every option upfront — useful for a related model with many rows. It requires wiring an options endpoint: drop django_admin_select_filter_path() into your root urlpatterns — no include() needed:

# urls.py
from django_admin_select_filter import django_admin_select_filter_path

urlpatterns = [
    ...
    django_admin_select_filter_path(),
]

Its route defaults to django_admin_select_filter/options/; pass route in case it collides with something else in your project:

django_admin_select_filter_path(route="custom-options/")

The endpoint stays reversible as admin_select_filter:options either way — django_admin_select_filter_path() still namespaces it internally via include(), that's just no longer something you have to write yourself. Pass route, view, name or as_view_kwargs to customize it further. If you pass a custom name, set async_call_url explicitly on every async_call filter to match it — filters resolve the default endpoint by its default name, and have no way to discover a custom one chosen here.

Each async_call filter reads that shared endpoint through BaseSelectFilter.async_call_url, resolved automatically at request time via reverse("admin_select_filter:options"). Set async_call_url on a specific filter to point it at a different view instead:

class AuthorFilter(ForeignKeyFilter):
    parameter_name = "author"
    async_call = True
    async_call_url = "/api/custom-author-options/"

If its route shares a prefix with your admin mount (e.g. both under admin/), list django_admin_select_filter_path() before path("admin/", admin.site.urls). Since Django 4.1, AdminSite registers a catch-all view (AdminSite.final_catch_all_view, enabled by default) that matches every otherwise-unmatched URL under its own prefix and raises Http404 itself — so if the admin mount comes first, it swallows requests to this app's options endpoint before its URLs ever get a chance to match, and you'll see a 404 with a full HTML body (the admin's own "Page not found" page) instead of this app's JSON response.

The filter template loads jQuery/Select2 from Django admin's bundled vendor assets on demand, so no extra JS dependency is required. Make sure django.contrib.staticfiles is installed and configured.

The endpoint only serves filters with async_call = True and requires the requesting user to have view permission on the target model (checked via ModelAdmin.has_view_permission) — anonymous or unprivileged requests get a 403, and a parameter_name matching a non-async filter gets a 404. It looks the model up across every registered AdminSite (not just the default django.contrib.admin.site), so a project using its own AdminSite works too.

By default it does this by calling get_list_filter() on every matching ModelAdmin on every request. For a project with many admins or filters, pass as_view_kwargs={"use_registry": True} to django_admin_select_filter_path() instead: it resolves filters from a {site: {app_label: {model_name: {parameter_name: filter}}}} map built once and cached for the process's lifetime (admin registrations are static after startup), turning that per-request scan into a single dict lookup. The trade-off: it calls get_list_filter(request=None) while building the cache, so a get_list_filter() override that depends on the request isn't supported in this mode.

Contributing

See CONTRIBUTING.md for development setup, running the test suite (including the browser-driven e2e tests), and pre-commit hooks.

Metadata

Release files for django-admin-select-filter 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-admin-select-filter 0.1.0
File Size Uploaded
django_admin_select_filter-0.1.0.tar.gz 181.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-admin-select-filter 0.1.0
File Interpreter ABI Platform
django_admin_select_filter-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 202.4 kB

Release files / django_admin_select_filter-0.1.0.tar.gz

Download URL django_admin_select_filter-0.1.0.tar.gz
Size 181.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d1306cdc52dc75d2149ee512aaa53470c86fcdff5330fe3048d3f512303086ed
BLAKE2b-256 checksum
How to use checksums
07ad71abf98bd4f0e8d0b9f5d580028b6ab9755e23f55aa98eee5eb52fb94b99
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / django_admin_select_filter-0.1.0-py3-none-any.whl

Download URL django_admin_select_filter-0.1.0-py3-none-any.whl
Size 20.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5b25f3b152d77e76b47b832a1b01026f4b19499b05cf11bb171850323ebf5a62
BLAKE2b-256 checksum
How to use checksums
5bce40939c37de2a1d8fda06c47d2f4088d78865a3c16c891f31d694c731fa0b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page