Customizing Django Admin for Productivity
python backend django
The Django admin is often underestimated. Out of the box, it’s basic CRUD. But with customization, it becomes a powerful internal tool.
Here’s how to unlock its potential.
Beyond Basic Registration
Instead of:
admin.site.register(Book)
Do this:
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'published_date', 'price']
list_filter = ['published_date', 'category']
search_fields = ['title', 'author__name', 'isbn']
ordering = ['-published_date']
Now you have a functional interface for browsing books.
List Display Customization
Computed Fields
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'customer', 'total_display', 'status', 'created']
@admin.display(description='Total', ordering='total')
def total_display(self, obj):
return f'${obj.total:.2f}'
Colored Status Badges
from django.utils.html import format_html
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'customer', 'status_badge']
@admin.display(description='Status')
def status_badge(self, obj):
colors = {
'pending': 'orange',
'completed': 'green',
'cancelled': 'red',
}
return format_html(
'<span style="color: {};">{}</span>',
colors.get(obj.status, 'black'),
obj.status.upper()
)
Filtering and Search
Custom Filters
class DecadeFilter(admin.SimpleListFilter):
title = 'decade'
parameter_name = 'decade'
def lookups(self, request, model_admin):
return [
('2020s', '2020s'),
('2010s', '2010s'),
('2000s', '2000s'),
]
def queryset(self, request, queryset):
if self.value() == '2020s':
return queryset.filter(published_date__year__gte=2020)
# ... other decades
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_filter = [DecadeFilter, 'category']
Related Field Search
search_fields = [
'title',
'author__name', # Foreign key
'author__email',
'tags__name', # Many-to-many
]
Inline Editing
Edit related objects on the same page:
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 1
readonly_fields = ['subtotal']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
inlines = [OrderItemInline]
Stacked vs Tabular
class OrderItemInline(admin.StackedInline): # Vertical layout
model = OrderItem
class OrderItemInline(admin.TabularInline): # Horizontal/table layout
model = OrderItem
Form Customization
Field Layout with Fieldsets
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
fieldsets = [
(None, {
'fields': ['name', 'sku', 'price']
}),
('Details', {
'fields': ['description', 'category', 'tags'],
'classes': ['collapse'], # Collapsible section
}),
('Inventory', {
'fields': ['stock_count', 'reorder_level'],
}),
]
Custom Form
from django import forms
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
fields = '__all__'
def clean_price(self):
price = self.cleaned_data['price']
if price < 0:
raise forms.ValidationError("Price cannot be negative")
return price
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
Custom Actions
Bulk operations from the list view:
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
actions = ['mark_as_shipped', 'export_to_csv']
@admin.action(description='Mark selected orders as shipped')
def mark_as_shipped(self, request, queryset):
updated = queryset.update(status='shipped')
self.message_user(request, f'{updated} orders marked as shipped.')
@admin.action(description='Export to CSV')
def export_to_csv(self, request, queryset):
import csv
from django.http import HttpResponse
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="orders.csv"'
writer = csv.writer(response)
writer.writerow(['ID', 'Customer', 'Total', 'Status'])
for order in queryset:
writer.writerow([order.id, order.customer.name, order.total, order.status])
return response
Read-Only and Permissions
@admin.register(AuditLog)
class AuditLogAdmin(admin.ModelAdmin):
list_display = ['timestamp', 'user', 'action', 'model']
readonly_fields = ['timestamp', 'user', 'action', 'model', 'changes']
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
Performance Optimization
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ['title', 'author_name', 'category']
list_select_related = ['author', 'category'] # Avoid N+1
def author_name(self, obj):
return obj.author.name
# For large tables
list_per_page = 25
show_full_result_count = False # Skip COUNT(*) query
Custom Admin Site
Rebrand the entire admin:
# admin.py
class MyAdminSite(admin.AdminSite):
site_header = 'My Company Admin'
site_title = 'My Company'
index_title = 'Dashboard'
my_admin_site = MyAdminSite(name='myadmin')
my_admin_site.register(Product, ProductAdmin)
# urls.py
urlpatterns = [
path('admin/', my_admin_site.urls),
]
Third-Party Enhancements
- django-grappelli: Modern skin and extra widgets
- django-admin-interface: Customizable themes
- django-import-export: Excel/CSV import/export
- django-admin-rangefilter: Date range filters
- django-admin-autocomplete-filter: Autocomplete for filters
Final Thoughts
The Django admin is a productivity multiplier. A few hours of customization can replace weeks of building internal tools.
Start with list_display and search_fields. Add filters as needed. Create actions for common operations. Your team will thank you.
The admin is not just for admins.