ModelAdmin Customization

Difficulty: Intermediate

The ModelAdmin class provides extensive customization options for how models appear in the admin. By overriding class attributes and methods, you can create a polished, efficient admin interface tailored to your data management needs.

list_display controls which columns appear in the list view. It accepts a list of field names, callable attributes, or method names. You can include model methods and ModelAdmin methods to display computed values. For foreign keys, you can use double-underscore syntax indirectly by creating a method that accesses the related object's field.

list_filter adds a sidebar with filter options. It accepts a list of field names that become clickable filters. Django automatically creates appropriate filter widgets based on field type - date fields get date hierarchy filters, boolean fields get Yes/No filters, and foreign keys get dropdown filters. Custom filters can be created by subclassing admin.SimpleListFilter.

search_fields enables a search box that queries the specified fields. Use double-underscore syntax to search across related models: 'author__username' searches the author's username field. The search supports multiple terms (ANDed) and can search across multiple fields (ORed within each term).

fieldsets organize the edit form into collapsible sections. Each fieldset is a tuple of (name, options_dict). The options dict contains 'fields' (which fields to include) and optional 'classes' (CSS classes like 'collapse' for collapsible sections) and 'description' (help text for the section).

readonly_fields prevents specific fields from being edited. date_hierarchy adds a date-based drill-down navigation. ordering overrides the default sort order. list_per_page controls pagination. list_editable makes fields editable directly in the list view. prepopulated_fields auto-fills fields (like slug from title) using JavaScript.

Code examples

Comprehensive ModelAdmin

from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # List view configuration
    list_display = ['title', 'author', 'category', 'status', 'published_at', 'view_count']
    list_filter = ['status', 'category', 'published_at', 'author']
    search_fields = ['title', 'content', 'author__username']
    list_editable = ['status']  # Edit status directly in list view
    list_per_page = 25
    date_hierarchy = 'published_at'
    ordering = ['-published_at']

    # Form configuration
    prepopulated_fields = {'slug': ('title',)}
    readonly_fields = ['view_count', 'created_at', 'updated_at']
    autocomplete_fields = ['author', 'category']

    # Fieldsets organize the edit form
    fieldsets = [
        (None, {
            'fields': ('title', 'slug', 'author', 'category'),
        }),
        ('Content', {
            'fields': ('content', 'excerpt', 'featured_image'),
        }),
        ('Publishing', {
            'fields': ('status', 'published_at'),
            'classes': ('collapse',),
        }),
        ('Metadata', {
            'fields': ('view_count', 'created_at', 'updated_at'),
            'classes': ('collapse',),
            'description': 'Auto-generated metadata fields.',
        }),
    ]

This comprehensive admin configuration customizes both list and edit views. list_display shows key columns, list_filter adds sidebar filters, fieldsets organize the form into sections, and prepopulated_fields auto-fills slug from title.

Custom List Display Methods

from django.contrib import admin
from django.utils.html import format_html
from .models import Product

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'colored_price', 'stock_status', 'image_preview']

    @admin.display(description='Price', ordering='price')
    def colored_price(self, obj):
        color = 'green' if obj.price < 50 else 'red'
        return format_html(
            '<span style="color: {};">${}</span>',
            color,
            obj.price,
        )

    @admin.display(description='Stock', boolean=True)
    def stock_status(self, obj):
        return obj.stock > 0

    @admin.display(description='Image')
    def image_preview(self, obj):
        if obj.image:
            return format_html(
                '<img src="{}" style="max-height: 50px;" />',
                obj.image.url,
            )
        return '-'

Custom methods in ModelAdmin display computed values. @admin.display sets metadata like description (column header), ordering (enables sorting), and boolean (shows checkmark icons). format_html safely renders HTML in the admin.

Custom Filters

from django.contrib import admin
from django.utils import timezone
from .models import Article

class PublishedRecentlyFilter(admin.SimpleListFilter):
    title = 'published recently'
    parameter_name = 'recent'

    def lookups(self, request, model_admin):
        return [
            ('today', 'Today'),
            ('week', 'This week'),
            ('month', 'This month'),
        ]

    def queryset(self, request, queryset):
        now = timezone.now()
        if self.value() == 'today':
            return queryset.filter(published_at__date=now.date())
        elif self.value() == 'week':
            return queryset.filter(
                published_at__gte=now - timezone.timedelta(days=7)
            )
        elif self.value() == 'month':
            return queryset.filter(
                published_at__gte=now - timezone.timedelta(days=30)
            )
        return queryset

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_filter = ['status', 'category', PublishedRecentlyFilter]

Custom filters extend SimpleListFilter. lookups() returns the filter options. queryset() applies the selected filter. The custom filter appears alongside standard filters in the sidebar.

Override save_model and get_queryset

from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'status']

    def save_model(self, request, obj, form, change):
        """Auto-set author on creation."""
        if not change:  # Creating, not editing
            obj.author = request.user
        super().save_model(request, obj, form, change)

    def get_queryset(self, request):
        """Non-superusers only see their own articles."""
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(author=request.user)

    def get_readonly_fields(self, request, obj=None):
        """Make author readonly for non-superusers."""
        if not request.user.is_superuser:
            return ['author', 'status']
        return []

Override save_model() to set fields automatically. Override get_queryset() to filter visible objects per user. Override get_readonly_fields() for dynamic read-only fields based on user permissions.

Key points

Concepts covered

list_display, list_filter, search_fields, readonly_fields, fieldsets