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
| Option | Purpose |
|---|---|
| list_display | Columns in changelist view |
| list_filter | Sidebar filter widgets |
| search_fields | Searchable fields |
| ordering | Default sort |
| raw_id_fields | Use ID input instead of select |
| filter_horizontal | ManyToMany horizontal widget |
| readonly_fields | Non-editable fields |
| save_on_top | Show save buttons at top |
| show_full_result_count | Show exact count in changelist |