Django Admin Guide

Customize the Django admin interface with ModelAdmin options, inline models, custom actions, and advanced configurations.

1. Basic ModelAdmin

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

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # List view columns
    list_display  = ("title", "author", "status", "view_count", "created_at", "cover_preview")
    list_display_links = ("title",)       # clickable columns
    list_editable  = ("status",)          # inline editing in list
    list_per_page  = 25

    # Filtering
    list_filter   = ("status", "category", "created_at")
    search_fields = ("title", "content", "author__name")
    date_hierarchy = "created_at"

    # Detail view
    readonly_fields = ("slug", "view_count", "created_at", "updated_at")
    prepopulated_fields = {"slug": ("title",)}
    autocomplete_fields = ["tags"]

    fieldsets = (
        (None, {"fields": ("title", "slug", "author", "category")}),
        ("Content", {"fields": ("content", "cover_image"), "classes": ("wide",)}),
        ("Publishing", {"fields": ("status", "published_at"), "classes": ("collapse",)}),
        ("Metadata", {"fields": ("view_count", "created_at", "updated_at"), "classes": ("collapse",)}),
    )

    def cover_preview(self, obj):
        if obj.cover_image:
            return format_html('', obj.cover_image.url)
        return "-"
    cover_preview.short_description = "Cover"

2. Custom Admin Actions

from django.contrib import admin, messages

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    actions = ["publish_selected", "unpublish_selected", "export_csv"]

    @admin.action(description="Publish selected articles")
    def publish_selected(self, request, queryset):
        updated = queryset.filter(status="draft").update(status="published")
        self.message_user(request, f"{updated} article(s) published.", messages.SUCCESS)

    @admin.action(description="Unpublish selected articles")
    def unpublish_selected(self, request, queryset):
        queryset.update(status="draft")
        self.message_user(request, "Articles unpublished.", messages.WARNING)

    @admin.action(description="Export as CSV")
    def export_csv(self, request, queryset):
        import csv
        from django.http import HttpResponse
        response = HttpResponse(content_type="text/csv")
        response["Content-Disposition"] = 'attachment; filename="articles.csv"'
        writer = csv.writer(response)
        writer.writerow(["ID", "Title", "Author", "Status"])
        for obj in queryset:
            writer.writerow([obj.pk, obj.title, obj.author.name, obj.status])
        return response

3. Inline Models

from django.contrib import admin
from .models import Order, OrderItem

class OrderItemInline(admin.TabularInline):   # or StackedInline
    model = OrderItem
    extra = 1              # blank forms to show
    min_num = 0
    max_num = 20
    fields = ("product", "quantity", "unit_price", "subtotal")
    readonly_fields = ("subtotal",)

    def subtotal(self, obj):
        return obj.quantity * obj.unit_price if obj.quantity and obj.unit_price else 0
    subtotal.short_description = "Subtotal"

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    inlines = [OrderItemInline]
    list_display  = ("order_number", "customer", "total", "status", "created_at")
    list_filter   = ("status",)
    readonly_fields = ("order_number", "total", "created_at")

4. Custom Admin Forms

from django import forms
from django.contrib import admin

class ArticleAdminForm(forms.ModelForm):
    content = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 20, "cols": 80, "class": "vLargeTextField"}),
    )
    tags_input = forms.CharField(
        required=False,
        help_text="Comma-separated tags",
        label="Tags (text)",
    )

    class Meta:
        model = Article
        fields = "__all__"

    def clean_title(self):
        title = self.cleaned_data["title"]
        if len(title) < 5:
            raise forms.ValidationError("Title must be at least 5 characters.")
        return title

    def save(self, commit=True):
        instance = super().save(commit=False)
        # process tags_input before saving
        if commit:
            instance.save()
            tags = [t.strip() for t in self.cleaned_data.get("tags_input", "").split(",") if t.strip()]
            instance.tags.set([Tag.objects.get_or_create(name=t)[0] for t in tags])
        return instance

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    form = ArticleAdminForm

5. Overriding Admin Views

from django.contrib import admin
from django.urls import path
from django.shortcuts import render

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # Add custom URL
    def get_urls(self):
        urls = super().get_urls()
        custom = [
            path("analytics/", self.admin_site.admin_view(self.analytics_view), name="article_analytics"),
        ]
        return custom + urls

    def analytics_view(self, request):
        from django.db.models import Count, Sum
        stats = Article.objects.values("status").annotate(count=Count("id"), views=Sum("view_count"))
        context = {**self.admin_site.each_context(request), "stats": stats, "title": "Article Analytics"}
        return render(request, "admin/article_analytics.html", context)

    # Override queryset for current user
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if not request.user.is_superuser:
            qs = qs.filter(author=request.user)
        return qs

6. ModelAdmin Options Reference

OptionPurpose
list_displayColumns in changelist view
list_filterSidebar filter widgets
search_fieldsSearchable fields
orderingDefault sort
raw_id_fieldsUse ID input instead of select
filter_horizontalManyToMany horizontal widget
readonly_fieldsNon-editable fields
save_on_topShow save buttons at top
show_full_result_countShow exact count in changelist