Skip to content

A search (cmd+k) modal, for the Django admin UI, that searches your entire site.

License

Notifications You must be signed in to change notification settings

ahmedaljawahiry/django-admin-site-search

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

77 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

django-admin-site-search

Test Lint PyPI Coverage Python Coverage Javascript Code style Pre-Commit PyPI version Downloads PyPI license

A global/site search modal for the Django admin.

Preview/demo GIF

Features

  • ๐ŸŽฉ 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:
      1. model_char_fields (default): All CharField (and subclass) values, with __icontains.
      2. 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.

Requirements

  • Python 3.7 - 3.12.
  • Django 3.2 - 5.1.

Setup

1. Install

  1. Install with your package manager, e.g. pip install django-admin-site-search.
  2. Add admin_site_search to your INSTALLED_APPS setting.

2. Add View

  1. If you haven't already, override/extend the default AdminSite.
  2. Add the AdminSiteSearchView to your AdminSite:
from django.contrib import admin
from admin_site_search.views import AdminSiteSearchView

class MyAdminSite(AdminSiteSearchView, admin.AdminSite):
    ...

3. Add Templates

  1. If you haven't already, create admin/base_site.html in your templates/ directory.
    • Note: if your templates/ directory is inside of an app, then that app must appear in INSTALLED_APPS before your custom admin app.
  2. Include the admin_site_search templates:
{% 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 %}

Notes

  • Along with styles, admin_site_search/head.html loads Alpine JS.
    • This is bundled into /static/, to avoid external dependencies.
  • The placement of modal.html and button.html are not strict, though the former would ideally be in a top-level position.
    • Django 4.x exposes {% block header %} - this is preferable to footer.

Customisation

Class attributes

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" 

Methods

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"""

Examples

1. Skip models from search.

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_class

This can be adapted to skip multiple models, or applications.

2. Add TextField results to search.

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.

Screenshots

Desktop, light theme, modal open

Mobile, light theme, modal closed Mobile, dark theme, modal open