A global/site search modal for the Django admin.
- π© Works out-of-the-box, with minimal config.
- π Search performed on:
- App labels.
- Model labels and field attributes.
- Model instances, with two options for a search method:
model_char_fields(default): AllCharField(and subclass) values, with__icontains.admin_search_fields: Invoke each ModelAdmin's get_search_results(...) method.
- π Built-in auth: users can only search apps and models that they have permission to view.
- β‘ Results appear on-type, with throttling/debouncing to avoid excessive requests.
- πΉ Keyboard navigation (cmd+k, up/down, enter).
- β¨ Responsive, and supports dark/light mode.
- Django's built-in CSS vars are used to match your admin theme.
- Python 3.8 - 3.13.
- Django 3.2 - 5.2.
- Install with your package manager, e.g.
pip install django-admin-site-search. - Add
admin_site_searchto yourINSTALLED_APPSsetting.
- If you haven't already, override/extend the default AdminSite.
- Add the
AdminSiteSearchViewto your AdminSite:
from django.contrib import admin from admin_site_search.views import AdminSiteSearchView class MyAdminSite(AdminSiteSearchView, admin.AdminSite): ...- If you haven't already, create
admin/base_site.htmlin yourtemplates/directory.- Note: if your
templates/directory is inside of an app, then that app must appear inINSTALLED_APPSbefore your custom admin app.
- Note: if your
- Include the
admin_site_searchtemplates:
{% extends "admin/base_site.html" %} {% block extrahead %} {% include 'admin_site_search/head.html' %} {{ block.super }} {% endblock %} {% block footer %} {{ block.super }} {% include 'admin_site_search/modal.html' %} {% endblock %} {% block usertools %} {% include 'admin_site_search/button.html' %} {{ block.super }} {% endblock %}- Along with styles,
admin_site_search/head.htmlloads Alpine JS.- This is bundled into
/static/, to avoid external dependencies.
- This is bundled into
- The placement of
modal.htmlandbutton.htmlare not strict, though the former would ideally be in a top-level position.- Django 4.x exposes
{% block header %}- this is preferable tofooter.
- Django 4.x exposes
class MyAdminSite(AdminSiteSearchView, admin.AdminSite): # Sets the last part of the search route (`<admin_path>/search/`). site_search_path: str = "search/" # Set the search method/behaviour. site_search_method: Literal["model_char_fields", "admin_search_fields"] = "model_char_fields" def match_app( self, request, query: str, name: str ) -> bool: """DEFAULT: case-insensitive match the app name""" def match_model( self, request, query: str, name: str, object_name: str, fields: List[Field] ) -> bool: """DEFAULT: case-insensitive match the model and field attributes""" def match_objects( self, request, query: str, model_class: Model, model_fields: List[Field] ) -> QuerySet: """DEFAULT: Returns the QuerySet after performing an OR filter across all Char fields in the model.""" def filter_field( self, request, query: str, field: Field ) -> Optional[Q]: """DEFAULT: Returns a Q 'icontains' filter for Char fields, otherwise None Note: this method is only invoked if model_char_fields is the site_search_method.""" def get_model_queryset( self, request, model_class: Model, model_admin: Optional[ModelAdmin] ) -> QuerySet: """DEFAULT: Returns the model class' .objects.all() queryset.""" def get_model_class( self, request, app_label: str, model_dict: dict ) -> Optional[Model]: """DEFAULT: Retrieve the model class from the dict created by admin.AdminSite"""class CustomAdminSite(AdminSiteSearchView, admin.AdminSite): def get_model_class(self, *args, **kwargs) -> Optional[Model]: """Extends super() to skip the auth.User model""" model_class = super().get_model_class(*args, **kwargs) model_name = f"{model_class._meta.app_label}.{model_class._meta.object_name}" if model_name == "auth.User": return None return model_classThis can be adapted to skip multiple models, or applications.
class MyAdminSite(AdminSiteSearchView, admin.AdminSite): site_search_method: "model_char_fields" def filter_field(self, request, query: str, field: Field) -> Optional[Q]: """Extends super() to add TextField support to site search""" if isinstance(field, TextField): return Q(**{f"{field.name}__icontains": query}) return super().filter_field(query, field)Note that this isn't done by default for performance reasons: __icontains on a large number of text entries is suboptimal.



