# Django Admin Customization in 2026: Actions, Filters and Interview Questions > Master Django admin customization with custom actions, list filters, fieldsets, and inlines. Covers Django 6 admin features including facets, show_facets, and common interview questions. - Published: 2026-09-13 - Updated: 2026-09-13 - Author: Anthony Fillion-Maillet - Tags: django, django-admin, python, interview - Reading time: 9 min --- Django admin customization transforms the built-in admin interface from a scaffolded tool into a purpose-built back office for production applications. Django 6 ships with enhanced admin features including facet counts for filters, customizable password change forms, and Font Awesome icons, making a well-configured admin genuinely pleasant to use. > **Key Takeaway** > > ModelAdmin is the central class for all admin customization. Master `list_display`, `list_filter`, `search_fields`, `fieldsets`, and custom actions to handle 90% of admin configuration needs. ## ModelAdmin Basics: list_display and search_fields `list_display` controls which columns appear on the change list page. Beyond simple field names, it accepts callables decorated with `@admin.display()` for computed values. `search_fields` enables the search box that filters records using `icontains` by default. ```python # admin.py from django.contrib import admin from django.utils.html import format_html from .models import Article class ArticleAdmin(admin.ModelAdmin): list_display = ["title", "author_name", "status", "publish_date", "is_recent"] search_fields = ["title", "author__email", "content"] list_per_page = 50 list_select_related = ["author"] # Avoid N+1 queries @admin.display(description="Author", ordering="author__last_name") def author_name(self, obj): return f"{obj.author.first_name} {obj.author.last_name}" @admin.display(description="Recent?", boolean=True) def is_recent(self, obj): from django.utils import timezone return obj.publish_date >= timezone.now() - timezone.timedelta(days=7) admin.site.register(Article, ArticleAdmin) ``` The `@admin.display()` decorator replaces the older pattern of setting `short_description` as a function attribute. The `boolean=True` parameter renders a checkmark or X icon instead of True/False text. Using `list_select_related` prevents N+1 queries when accessing foreign key fields in `list_display`. ## Custom list_filter with SimpleListFilter `list_filter` activates sidebar filters on the change list page. Field names work for basic filtering, but `SimpleListFilter` handles complex filtering logic that spans multiple fields or requires custom queries. ```python # admin.py from datetime import date, timedelta from django.contrib import admin from django.utils.translation import gettext_lazy as _ from .models import Article class PublishDateFilter(admin.SimpleListFilter): title = _("publish period") parameter_name = "published" # URL parameter: ?published=this_week def lookups(self, request, model_admin): # Returns (value, label) tuples for the filter sidebar return [ ("this_week", _("This week")), ("this_month", _("This month")), ("this_year", _("This year")), ("unpublished", _("Not published")), ] def queryset(self, request, queryset): # Filter the queryset based on self.value() today = date.today() if self.value() == "this_week": return queryset.filter(publish_date__gte=today - timedelta(days=7)) if self.value() == "this_month": return queryset.filter( publish_date__year=today.year, publish_date__month=today.month ) if self.value() == "this_year": return queryset.filter(publish_date__year=today.year) if self.value() == "unpublished": return queryset.filter(publish_date__isnull=True) return queryset class ArticleAdmin(admin.ModelAdmin): list_filter = [ "status", PublishDateFilter, ("author", admin.RelatedOnlyFieldListFilter), # Show only authors with articles ] ``` `RelatedOnlyFieldListFilter` limits the author dropdown to authors that actually have articles, rather than listing every user in the database. This improves usability when the related model has many records. ## Django 6 Facets: Filter Counts in the Sidebar Django 6 introduced [facet counts](https://docs.djangoproject.com/en/6.0/ref/contrib/admin/filters/) that display the number of matching objects next to each filter option. The `show_facets` attribute controls this behavior. ```python # admin.py from django.contrib import admin from django.contrib.admin import ShowFacets from .models import Article class ArticleAdmin(admin.ModelAdmin): list_display = ["title", "status", "category"] list_filter = ["status", "category", "author"] show_facets = ShowFacets.ALWAYS # Always display counts # Alternative values: # ShowFacets.ALLOW - Show counts when ?_facets=1 in URL # ShowFacets.NEVER - Never show counts (for large datasets) ``` Facet counts execute additional COUNT queries for each filter option. On tables with millions of rows, set `show_facets = ShowFacets.NEVER` to avoid performance degradation. The counts update dynamically as filters are applied, showing how many records match the current filter combination. ## Custom Admin Actions for Bulk Operations [Admin actions](https://docs.djangoproject.com/en/6.0/ref/contrib/admin/actions/) perform bulk operations on selected objects. The `@admin.action` decorator sets permissions and the description shown in the dropdown. ```python # admin.py from django.contrib import admin from django.contrib import messages from django.utils.translation import ngettext from .models import Article @admin.action(description="Publish selected articles", permissions=["change"]) def publish_articles(modeladmin, request, queryset): # Exclude already published articles unpublished = queryset.exclude(status="published") updated = unpublished.update(status="published") modeladmin.message_user( request, ngettext( "%d article was published.", "%d articles were published.", updated, ) % updated, messages.SUCCESS, ) @admin.action(description="Export selected as JSON") def export_as_json(modeladmin, request, queryset): from django.http import HttpResponse from django.core import serializers response = HttpResponse(content_type="application/json") response["Content-Disposition"] = 'attachment; filename="articles.json"' serializers.serialize("json", queryset, stream=response) return response # Returning HttpResponse triggers file download class ArticleAdmin(admin.ModelAdmin): actions = [publish_articles, export_as_json] def get_actions(self, request): # Conditionally remove actions based on user permissions actions = super().get_actions(request) if not request.user.has_perm("articles.delete_article"): del actions["delete_selected"] return actions ``` Returning an `HttpResponse` from an action triggers a file download or redirects to a confirmation page. The `permissions` parameter restricts the action to users with specific model permissions. Override `get_actions()` to dynamically show or hide actions based on the current request. ## Fieldsets and Inline Models for Organized Forms `fieldsets` organizes the add and change forms into collapsible sections. `TabularInline` and `StackedInline` embed related models directly in the parent form, eliminating navigation between pages. ```python # admin.py from django.contrib import admin from .models import Article, ArticleImage, ArticleComment class ArticleImageInline(admin.TabularInline): model = ArticleImage extra = 1 # Number of empty forms to display max_num = 5 # Maximum images per article fields = ["image", "caption", "order"] class ArticleCommentInline(admin.StackedInline): model = ArticleComment extra = 0 readonly_fields = ["created_at", "author"] can_delete = True class ArticleAdmin(admin.ModelAdmin): fieldsets = [ ( None, { "fields": ["title", "slug", "author"], }, ), ( "Content", { "fields": ["content", "excerpt"], "classes": ["wide"], # Full-width fields }, ), ( "Publishing", { "fields": ["status", "publish_date", "category", "tags"], "classes": ["collapse"], # Collapsed by default }, ), ( "SEO", { "fields": ["meta_title", "meta_description"], "classes": ["collapse"], "description": "Search engine optimization fields", }, ), ] inlines = [ArticleImageInline, ArticleCommentInline] prepopulated_fields = {"slug": ["title"]} # Auto-generate slug from title autocomplete_fields = ["author", "category"] # AJAX-powered select filter_horizontal = ["tags"] # Widget for M2M selection admin.site.register(Article, ArticleAdmin) ``` `TabularInline` displays related objects in a compact table format, suitable for simple models with few fields. `StackedInline` shows each related object as a separate form block, better for models with many fields. The `autocomplete_fields` option replaces the default dropdown with an AJAX search widget, essential when the related model has thousands of records. ## Customizing the Admin Site Subclassing `AdminSite` allows multiple admin interfaces with different branding, permissions, or model registrations. Django 6 added `password_change_form` customization for stricter password policies. ```python # admin.py from django.contrib import admin from django.contrib.admin import AdminSite from .models import Article, Category class ContentAdminSite(AdminSite): site_header = "Content Management" site_title = "CMS" index_title = "Dashboard" def has_permission(self, request): # Restrict to staff in the 'editors' group return ( request.user.is_active and request.user.is_staff and request.user.groups.filter(name="editors").exists() ) content_admin = ContentAdminSite(name="content_admin") content_admin.register(Article, ArticleAdmin) content_admin.register(Category) # In urls.py: # path('content-admin/', content_admin.urls), ``` Each `AdminSite` instance has its own URL namespace, login page, and set of registered models. This pattern suits applications with distinct user roles where editors should not see user management or system settings. ## Interview Questions: Django Admin Customization These questions frequently appear in Django technical interviews. Understanding the underlying mechanisms, not just the syntax, separates mid-level candidates from senior engineers. **Q: How do you add a computed column to list_display that cannot be sorted?** The `@admin.display()` decorator accepts an `ordering` parameter that specifies which database field to use for sorting. Without it, the column header is not clickable. Computed values that aggregate multiple records or call external services cannot be sorted because there is no underlying database column to order by. **Q: What is the performance impact of facet counts, and when should they be disabled?** Facets execute one COUNT query per filter option, multiplied by the number of active filters. On a table with 10 million rows and 5 filter options each on 3 fields, that is 15 additional COUNT queries per page load. Set `show_facets = ShowFacets.NEVER` when any filter targets a table over 100,000 rows without appropriate indexes. **Q: How do admin actions differ from custom admin views?** Actions operate on a queryset of selected objects and integrate with the change list's selection checkboxes. Custom admin views are standalone pages accessed via custom URLs, suitable for reports, imports, or dashboards that do not operate on selected records. Actions should not perform long-running operations because they block the request. **Q: Why use RelatedOnlyFieldListFilter instead of the default filter?** The default `RelatedFieldListFilter` queries all objects in the related table. When a ForeignKey points to a User model with 100,000 records, the filter dropdown loads all 100,000 options, freezing the browser. `RelatedOnlyFieldListFilter` queries only the values that exist in the current model's records, typically a much smaller set. ## Common Mistakes to Avoid in Production Several patterns that work in development cause issues at scale. ```python # admin.py from django.contrib import admin from .models import Order class OrderAdmin(admin.ModelAdmin): # Mistake: Accessing related fields without list_select_related # This causes N+1 queries, one per row in the list view list_display = ["id", "customer_email", "total"] # Fix: Prefetch related objects list_select_related = ["customer"] def customer_email(self, obj): return obj.customer.email # Mistake: search_fields on unindexed large text columns # search_fields = ["notes"] # Slow on large tables # Fix: Limit search to indexed fields search_fields = ["id", "customer__email"] # Mistake: Expensive computation in list_display # def order_total(self, obj): # return sum(item.price * item.qty for item in obj.items.all()) # N+1 # Fix: Use annotation in get_queryset def get_queryset(self, request): from django.db.models import Sum, F qs = super().get_queryset(request) return qs.annotate(computed_total=Sum(F("items__price") * F("items__qty"))) @admin.display(description="Total", ordering="computed_total") def total(self, obj): return obj.computed_total ``` Overriding `get_queryset()` to add annotations keeps computed values in a single query rather than hitting the database once per row. Profile the admin with Django Debug Toolbar to catch N+1 queries before they reach production. ## Third-Party Admin Themes: Django Unfold [Django Unfold](https://unfoldadmin.com/) is the most actively maintained admin theme for Django 6, built with Tailwind CSS. It extends rather than replaces the standard admin, so existing `ModelAdmin` configurations continue working. ```python # settings.py INSTALLED_APPS = [ "unfold", # Must come before django.contrib.admin "unfold.contrib.filters", # Advanced filter widgets "unfold.contrib.forms", # Enhanced form widgets "django.contrib.admin", # ... ] ``` Unfold adds dark mode, responsive layouts, sidebar navigation, and advanced filter types like `RangeNumericFilter` and `RangeDateFilter`. The HTMX and Alpine.js integration provides interactivity without the overhead of a full SPA framework. For teams that need a polished admin without building a custom dashboard, Unfold saves weeks of frontend work. ## What to Remember About Django Admin Customization - `list_display` with `@admin.display()` controls columns; add `ordering` for sortable computed values - `SimpleListFilter` handles custom filter logic that spans multiple fields or requires date ranges - Django 6 facets show filter counts; disable with `ShowFacets.NEVER` on large tables to avoid COUNT query overhead - Admin actions with `@admin.action(permissions=[...])` handle bulk operations; return `HttpResponse` for file exports - Use `list_select_related` and annotated querysets in `get_queryset()` to prevent N+1 queries in list views - `RelatedOnlyFieldListFilter` prevents browser freezes when filtering by ForeignKey to large tables - Practice these patterns with [Django interview questions](/technologies/django/interview-questions/django-admin) that target admin configuration specifically --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/django/django-admin-customization-2026