Django Admin Customization: The 80-20 Guide#
Complete guide to customizing Django admin interface with the most important features.
🎯 Basic Admin Setup#
Register Model#
# myapp/admin.py
from django.contrib import admin
from .models import Post, Category, Tag
# Simple registration
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Tag)
Basic Admin Class#
# myapp/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
pass
🏷️ Model str Method#
# myapp/models.py
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.title
# This is what shows in admin dropdowns and list views
📋 List Display (80-20 Most Important)#
Basic List Display#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['id', 'title', 'author', 'is_published', 'created_at']
# Shows these columns in list view
List Display with Methods#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'status_display', 'view_count']
def status_display(self, obj):
return 'Published' if obj.is_published else 'Draft'
status_display.short_description = 'Status'
status_display.admin_order_field = 'is_published'
def view_count(self, obj):
return obj.views
view_count.short_description = 'Views'
view_count.admin_order_field = 'views'
List Display with format_html#
from django.utils.html import format_html
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'status_badge', 'actions']
def status_badge(self, obj):
if obj.is_published:
return format_html(
'<span style="color: green; font-weight: bold;">✓ Published</span>'
)
return format_html(
'<span style="color: red;">✗ Draft</span>'
)
status_badge.short_description = 'Status'
def actions(self, obj):
return format_html(
'<a href="/admin/myapp/post/{}/change/">Edit</a> | '
'<a href="/admin/myapp/post/{}/delete/">Delete</a>',
obj.id, obj.id
)
actions.short_description = 'Actions'
🔍 List Filter#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_filter = ['is_published', 'created_at', 'author', 'category']
# Adds filter sidebar
# Date hierarchy (adds date drill-down)
date_hierarchy = 'created_at'
# Custom filter
list_filter = ['is_published', ('author', admin.RelatedOnlyFieldListFilter)]
🔎 Search Fields#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
search_fields = ['title', 'content']
# Adds search box
# Search in related fields
search_fields = ['title', 'author__username', 'category__name']
📝 Fields & Fieldsets#
Fields (Simple)#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
fields = ['title', 'content', 'author', 'is_published']
# Only show these fields, in this order
# Exclude fields
exclude = ['slug', 'views']
Fieldsets (Organized)#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
fieldsets = (
('Basic Information', {
'fields': ('title', 'content', 'excerpt')
}),
('Metadata', {
'fields': ('author', 'category', 'tags'),
'classes': ('collapse',) # Collapsible section
}),
('Publishing', {
'fields': ('is_published', 'published_at'),
}),
)
✏️ Readonly Fields#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
readonly_fields = ['created_at', 'updated_at', 'views']
# Fields that can't be edited
# Readonly in detail, editable in list
def get_readonly_fields(self, request, obj=None):
if obj: # Editing existing object
return self.readonly_fields + ['slug']
return self.readonly_fields
🎨 Inline Admin#
TabularInline#
# For ManyToMany or ForeignKey (many side)
class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # Number of empty forms
fields = ['author', 'content', 'created_at']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]
StackedInline#
class CommentInline(admin.StackedInline):
model = Comment
extra = 1
fields = ['author', 'content']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]
📊 List Editable#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_editable = ['is_published', 'is_featured']
# Edit directly from list view
# Must be in list_display but not first field
🔢 Pagination & Ordering#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_per_page = 25 # Items per page
list_max_show_all = 100 # Max items to show "Show all"
ordering = ['-created_at'] # Default ordering
sortable_by = ['title', 'created_at', 'author'] # Sortable columns
🎯 Actions#
Built-in Actions#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
actions = ['make_published', 'make_draft']
def make_published(self, request, queryset):
updated = queryset.update(is_published=True)
self.message_user(request, f'{updated} posts published.')
make_published.short_description = 'Mark selected as published'
def make_draft(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(request, f'{updated} posts marked as draft.')
make_draft.short_description = 'Mark selected as draft'
Action with Confirmation#
from django.contrib import messages
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
actions = ['delete_selected_posts']
def delete_selected_posts(self, request, queryset):
count = queryset.count()
queryset.delete()
messages.success(request, f'{count} posts deleted.')
delete_selected_posts.short_description = 'Delete selected posts'
🎨 Custom Templates#
Override Admin Templates#
# Create: templates/admin/myapp/post/change_form.html
# Extends admin template and customizes
<!-- templates/admin/myapp/post/change_form.html -->
{% extends "admin/change_form.html" %}
{% block content %}
{{ block.super }}
<!-- Your custom content -->
{% endblock %}
🔐 Permissions#
Custom Permissions#
# In model
class Post(models.Model):
class Meta:
permissions = [
('can_publish', 'Can publish posts'),
('can_feature', 'Can feature posts'),
]
Check Permissions in Admin#
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
return request.user.has_perm('myapp.add_post')
def has_change_permission(self, request, obj=None):
if obj and obj.author != request.user:
return request.user.is_staff
return True
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
📋 Complete Example#
# myapp/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Post, Category, Tag
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']
def post_count(self, obj):
return obj.post_set.count()
post_count.short_description = 'Posts'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# List view
list_display = ['id', 'title', 'author', 'category', 'status_badge',
'views', 'created_at', 'actions']
list_filter = ['is_published', 'category', 'created_at', 'author']
search_fields = ['title', 'content', 'author__username']
date_hierarchy = 'created_at'
list_editable = ['is_published']
list_per_page = 25
ordering = ['-created_at']
# Detail view
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'content', 'excerpt')
}),
('Metadata', {
'fields': ('author', 'category', 'tags'),
'classes': ('collapse',)
}),
('Publishing', {
'fields': ('is_published', 'is_featured', 'published_at')
}),
('Statistics', {
'fields': ('views', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
prepopulated_fields = {'slug': ('title',)}
readonly_fields = ['created_at', 'updated_at', 'views']
filter_horizontal = ['tags'] # Better UI for ManyToMany
# Actions
actions = ['make_published', 'make_draft', 'make_featured']
def status_badge(self, obj):
if obj.is_published:
return format_html(
'<span style="color: green; font-weight: bold;">✓ Published</span>'
)
return format_html(
'<span style="color: orange;">✗ Draft</span>'
)
status_badge.short_description = 'Status'
status_badge.admin_order_field = 'is_published'
def actions(self, obj):
return format_html(
'<a href="/admin/myapp/post/{}/change/">Edit</a>',
obj.id
)
actions.short_description = 'Actions'
def make_published(self, request, queryset):
updated = queryset.update(is_published=True)
self.message_user(request, f'{updated} posts published.')
make_published.short_description = 'Mark selected as published'
def make_draft(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(request, f'{updated} posts marked as draft.')
make_draft.short_description = 'Mark selected as draft'
def make_featured(self, request, queryset):
updated = queryset.update(is_featured=True)
self.message_user(request, f'{updated} posts featured.')
make_featured.short_description = 'Mark selected as featured'
🎯 80-20 Admin Features Summary#
Most Used (80% of cases):
1. ✅ list_display - What columns to show
2. ✅ list_filter - Filter sidebar
3. ✅ search_fields - Search functionality
4. ✅ fieldsets - Organize form fields
5. ✅ readonly_fields - Non-editable fields
6. ✅ list_editable - Edit from list view
7. ✅ prepopulated_fields - Auto-generate slugs
8. ✅ filter_horizontal/vertical - Better ManyToMany UI
9. ✅ inlines - Edit related objects
10. ✅ actions - Bulk actions
✅ Best Practices#
- ✅ Always define str - Makes admin readable
- ✅ Use format_html - For safe HTML in list_display
- ✅ Organize with fieldsets - Better UX
- ✅ Use prepopulated_fields - For slugs
- ✅ Add search_fields - Makes admin usable
- ✅ Use list_filter - For common filters
- ✅ Set readonly_fields - Prevent accidental edits
- ✅ Create actions - For bulk operations
✅ Next Steps#
- Learn Models to understand model structure
- Learn Forms for custom form handling in admin
- Learn Settings & Production for production admin
Pro Tip: Use format_html() for safe HTML rendering in admin. Always define __str__ method in models - it's used everywhere in admin!