Django’s admin interface is one of the framework’s most underestimated features. Teams that treat it as a quick scaffold miss the fact that it handles daily operations at thousands of companies, from content publishing and order management to user support and data auditing. The difference between a default admin and a genuinely useful one comes down to customization: list displays that surface the right data, filters that match real workflows, inline editing for related objects, custom actions for batch operations, and sensible permission boundaries. This guide covers the practical customizations I apply to every Django project, along with the patterns that keep the admin maintainable as the data model grows. For a broader view of admin patterns, see the Admin hub.
The admin is not a replacement for a custom frontend. But for internal tools, content management, and operational dashboards, a well-configured admin is faster to build and easier to maintain than a custom React application for the same purpose.
Model registration and list display
The simplest admin registration gives you basic CRUD. A production admin needs more:
from django.contrib import admin
from .models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'category', 'price', 'is_active', 'created_at']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'sku', 'description']
list_editable = ['is_active', 'price']
list_per_page = 50
date_hierarchy = 'created_at'
prepopulated_fields = {'slug': ('name',)}
ordering = ['-created_at']
list_display controls what columns appear in the list view. Include the fields your team actually uses to triage and locate records. list_filter adds sidebar filters. search_fields enables the search bar. list_editable allows inline editing directly from the list view without opening each record.
Custom display methods
When you need computed columns or formatted output:
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['order_number', 'customer_name', 'total_display', 'status', 'created_at']
@admin.display(description='Customer', ordering='customer__last_name')
def customer_name(self, obj):
return f"{obj.customer.first_name} {obj.customer.last_name}"
@admin.display(description='Total')
def total_display(self, obj):
return f"${obj.total:,.2f}"
The @admin.display decorator provides column headers and allows sorting on related fields or computed values.
Fieldsets and form layout
Group related fields into logical sections for the detail view:
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
fieldsets = [
(None, {
'fields': ['name', 'slug', 'category']
}),
('Pricing', {
'fields': ['price', 'compare_at_price', 'tax_class']
}),
('Content', {
'fields': ['description', 'features', 'specifications'],
'classes': ['collapse']
}),
('Status', {
'fields': ['is_active', 'published_at', 'created_at'],
}),
]
readonly_fields = ['created_at']
The collapse class hides a section by default, keeping the form clean for common workflows while making additional fields accessible when needed.
Inline editing for related objects
Inlines let you edit related objects directly from the parent’s detail page:
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
readonly_fields = ['line_total']
fields = ['product', 'quantity', 'unit_price', 'line_total']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
inlines = [OrderItemInline]
Use TabularInline for compact tabular display and StackedInline for forms with more fields per item. Set extra = 0 so empty rows do not clutter the interface.
Custom admin actions
Actions let users perform batch operations on selected records:
@admin.action(description="Mark selected as published")
def make_published(modeladmin, request, queryset):
updated = queryset.update(status='published', published_at=timezone.now())
modeladmin.message_user(request, f"{updated} items published.")
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
actions = [make_published]
Custom actions are the simplest way to give operations teams the ability to perform bulk updates without writing custom views.
Permissions and access control
The admin respects Django’s permission system. Each model gets add, change, delete, and view permissions. Control access per user or group:
@admin.register(SensitiveRecord)
class SensitiveRecordAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
def has_change_permission(self, request, obj=None):
if obj and obj.is_locked:
return False
return super().has_change_permission(request, obj)
For more complex permission patterns, see the authentication patterns guide.
Admin site configuration
Customize the admin header, title, and branding:
admin.site.site_header = "Operations Dashboard"
admin.site.site_title = "Ops Admin"
admin.site.index_title = "Management"
For visual theming beyond text changes, override admin templates by placing custom templates in templates/admin/. Django’s template loader checks project-level templates before the default admin templates.
Raw ID fields for large relation sets
When a ForeignKey or ManyToMany field references a table with thousands of rows, the default dropdown becomes unusable. Use raw_id_fields:
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
raw_id_fields = ['customer', 'assigned_agent']
This replaces the dropdown with a text input and a lookup popup, which works regardless of table size.
Frequently asked questions
Should I build a custom admin or use Django’s built-in one? Start with the built-in admin. Customize it until it no longer fits the workflow. Only then consider a custom frontend. Many teams overestimate the need for a custom admin interface and underestimate the maintenance cost.
How do I add charts or dashboards to the admin?
Override the admin index template or create a custom admin view. Libraries like django-admin-charts or simple inline JavaScript with a charting library can add visualizations without replacing the entire admin.
Can I restrict which fields certain users see?
Override get_fieldsets() or get_readonly_fields() to return different configurations based on the request user’s permissions or group membership.
How do I handle audit logging in the admin?
Django’s LogEntry model tracks admin changes automatically. For more detailed audit trails, use django-auditlog or django-simple-history, which record field-level changes.
Is the Django admin secure enough for production? Yes, when properly configured. Enforce strong passwords, use two-factor authentication, restrict admin access to trusted networks or VPNs, and keep Django updated. The admin uses Django’s full security middleware stack.