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()
        )

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']
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

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.

All posts