Skip to content

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#

  1. Always define str - Makes admin readable
  2. Use format_html - For safe HTML in list_display
  3. Organize with fieldsets - Better UX
  4. Use prepopulated_fields - For slugs
  5. Add search_fields - Makes admin usable
  6. Use list_filter - For common filters
  7. Set readonly_fields - Prevent accidental edits
  8. Create actions - For bulk operations

✅ Next Steps#


Pro Tip: Use format_html() for safe HTML rendering in admin. Always define __str__ method in models - it's used everywhere in admin!