Django Admin Customization
Django's admin interface is a powerful built-in feature that automatically generates a user interface for managing your application's data. While the default admin is already useful, customizing it can significantly improve your workflow and make it more suited to your project's specific requirements.
Introduction to Django Admin Customization
Django's admin site is designed to be extensible and customizable. By default, it provides basic CRUD (Create, Read, Update, Delete) operations for your models, but you can enhance its functionality by:
- Customizing the display and behavior of model data
- Changing the appearance of the admin interface
- Adding custom functionality and actions
- Controlling access permissions
- Organizing models in a more intuitive way
In this tutorial, we'll explore various techniques to transform the default Django admin into a tailored administrative tool for your application.
Prerequisites
Before diving into customization, ensure you have:
- Django installed (
pip install django
) - A Django project set up
- Basic understanding of Django models
- Admin site enabled in your project
Basic Admin Registration
Let's start with a simple model and the default admin registration:
# models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
in_stock = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
The basic admin registration looks like this:
# admin.py
from django.contrib import admin
from .models import Product
admin.site.register(Product)
This gives you a basic admin interface for the Product model with minimal functionality.
ModelAdmin Class
To customize how a model appears in the admin, we use the ModelAdmin
class:
# admin.py
from django.contrib import admin
from .models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'price', 'in_stock', 'created_at')
list_filter = ('in_stock', 'created_at')
search_fields = ('name', 'description')
ordering = ('-created_at',)
With this customization:
list_display
controls which fields appear in the list viewlist_filter
adds filters in the right sidebarsearch_fields
enables searching through specified fieldsordering
sets the default ordering of records
Customizing List Views
Let's enhance the list view further:
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'price', 'in_stock', 'created_at', 'price_category')
list_editable = ('price', 'in_stock')
list_display_links = ('name',)
list_per_page = 20
def price_category(self, obj):
if obj.price < 10:
return 'Budget'
elif obj.price < 50:
return 'Mid-range'
else:
return 'Premium'
price_category.short_description = 'Category'
New features used:
list_editable
fields can be edited directly from the list viewlist_display_links
specifies which field(s) link to the change viewlist_per_page
controls pagination- Custom methods can be added to
list_display
for computed values
Customizing Detail Views
You can also customize how the detail/edit form appears:
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# List view customizations...
# Detail view customizations
fieldsets = (
('Basic Information', {
'fields': ('name', 'description')
}),
('Pricing and Availability', {
'fields': ('price', 'in_stock'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at',)
The fieldsets
option groups fields into sections with headings, and can apply CSS classes like collapse
to make sections collapsible. The readonly_fields
setting prevents certain fields from being edited.
Adding Custom Actions
Admin actions allow you to perform operations on multiple selected items:
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# Other customizations...
actions = ['mark_as_out_of_stock', 'apply_discount']
def mark_as_out_of_stock(self, request, queryset):
updated = queryset.update(in_stock=False)
self.message_user(request, f'{updated} products marked as out of stock.')
mark_as_out_of_stock.short_description = 'Mark selected products as out of stock'
def apply_discount(self, request, queryset):
for product in queryset:
product.price = product.price * 0.9 # 10% discount
product.save()
self.message_user(request, f'10% discount applied to {queryset.count()} products.')
apply_discount.short_description = 'Apply 10% discount'
This adds two actions to the dropdown menu in the list view, allowing bulk operations on selected products.
Inline Editing with Admin Inlines
If you have related models, you can edit them together using inlines. Let's add a ProductImage
model:
# models.py
class ProductImage(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
image = models.ImageField(upload_to='products/')
alt_text = models.CharField(max_length=100)
def __str__(self):
return f"Image for {self.product.name}"
Now we can add inline editing in the admin:
# admin.py
from .models import Product, ProductImage
class ProductImageInline(admin.TabularInline):
model = ProductImage
extra = 1 # Number of empty forms to display
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# Other customizations...
inlines = [ProductImageInline]
With this setup, you can edit product images directly in the product edit page. There are two types of inlines:
TabularInline
: Displays related objects in a table formatStackedInline
: Displays each related object in a style similar to the model's detail form
Admin Site Customization
You can also customize the entire admin site:
# admin.py
from django.contrib import admin
from django.contrib.admin import AdminSite
class MyAdminSite(AdminSite):
site_header = 'My E-commerce Administration'
site_title = 'E-commerce Admin'
index_title = 'Dashboard'
site_url = '/shop/' # Link to frontend
admin_site = MyAdminSite(name='myadmin')
# Register your models with your custom admin site
admin_site.register(Product, ProductAdmin)
admin_site.register(ProductImage)
Then update your URL configuration:
# urls.py
from django.urls import path
from myapp.admin import admin_site
urlpatterns = [
path('admin/', admin_site.urls),
# Other URL patterns...
]
Advanced Customizations
List Filters
Create custom filters for the admin list view:
class PriceRangeFilter(admin.SimpleListFilter):
title = 'price range'
parameter_name = 'price_range'
def lookups(self, request, model_admin):
return (
('budget', 'Budget (under $10)'),
('mid', 'Mid-range ($10-$50)'),
('premium', 'Premium (over $50)'),
)
def queryset(self, request, queryset):
if self.value() == 'budget':
return queryset.filter(price__lt=10)
if self.value() == 'mid':
return queryset.filter(price__gte=10, price__lt=50)
if self.value() == 'premium':
return queryset.filter(price__gte=50)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# Other customizations...
list_filter = ('in_stock', PriceRangeFilter)
Custom Forms and Validation
Use custom forms in the admin for additional validation:
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 must be greater than zero!")
return price
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
# Other customizations...
Admin Templates
For deeper customization, you can override admin templates:
- Create a
templates/admin/
directory in your app - Create template files that match the names of the admin templates you want to override
For example, to customize the change form template:
{/* templates/admin/myapp/product/change_form.html */}
{% extends "admin/change_form.html" %}
{% load i18n admin_urls %}
{% block object-tools-items %}
<li>
<a href="{% url 'admin:generate_product_report' original.pk %}" class="button">
{% trans "Generate Report" %}
</a>
</li>
{{ block.super }}
{% endblock %}
Then add the corresponding URL:
# urls.py
from django.urls import path
from .admin_views import generate_product_report
urlpatterns = [
# Other URL patterns...
path('admin/myapp/product/<int:pk>/report/', generate_product_report, name='admin:generate_product_report'),
]
Practical Example: A Complete Blog Admin
Let's implement a comprehensive admin for a blog application:
# models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Categories"
class Post(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('published', 'Published'),
)
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
categories = models.ManyToManyField(Category)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
name = models.CharField(max_length=100)
email = models.EmailField()
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
approved = models.BooleanField(default=False)
def __str__(self):
return f'Comment by {self.name} on {self.post.title}'
Now let's create a comprehensive admin interface:
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Category, Post, Comment
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ('name', 'email', 'body', 'created_at')
can_delete = False
def has_add_permission(self, request, obj=None):
return False
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'author', 'display_categories', 'created_at', 'comment_count')
list_filter = ('status', 'created_at', 'categories', 'author')
search_fields = ('title', 'content')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'created_at'
filter_horizontal = ('categories',)
inlines = [CommentInline]
actions = ['make_published']
fieldsets = (
(None, {
'fields': ('title', 'slug', 'author')
}),
('Content', {
'fields': ('content',)
}),
('Categorization', {
'fields': ('categories', 'status')
}),
)
def display_categories(self, obj):
return ", ".join([category.name for category in obj.categories.all()])
display_categories.short_description = 'Categories'
def comment_count(self, obj):
count = obj.comments.count()
approved_count = obj.comments.filter(approved=True).count()
return format_html(
'<span style="color: {};">{} ({} approved)</span>',
'green' if approved_count == count else 'red',
count,
approved_count
)
comment_count.short_description = 'Comments'
def make_published(self, request, queryset):
updated = queryset.update(status='published')
self.message_user(request, f'{updated} posts have been marked as published.')
make_published.short_description = "Mark selected posts as published"
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'post', 'created_at', 'approved')
list_filter = ('approved', 'created_at')
search_fields = ('name', 'email', 'body')
actions = ['approve_comments', 'disapprove_comments']
def approve_comments(self, request, queryset):
updated = queryset.update(approved=True)
self.message_user(request, f'{updated} comments have been approved.')
approve_comments.short_description = "Approve selected comments"
def disapprove_comments(self, request, queryset):
updated = queryset.update(approved=False)
self.message_user(request, f'{updated} comments have been disapproved.')
disapprove_comments.short_description = "Disapprove selected comments"
In this comprehensive example, we've implemented:
- Slug auto-generation with
prepopulated_fields
- Inline editing for related models
- Custom display methods with HTML formatting
- Admin actions for bulk operations
- Fieldsets for organizing the edit form
- Read-only inlines with permission control
- Date hierarchies for time-based navigation
- And many more customizations!
Summary
Django's admin interface is highly customizable, allowing you to transform the default interface into a powerful, project-specific administrative tool. We've covered:
- Basic model registration
- Customizing list and detail views
- Adding custom actions and computed fields
- Inline editing for related models
- Creating custom admin sites
- Advanced customizations like filters, forms, and template overrides
- A practical blog admin example
With these techniques, you can create an admin interface that's not just functional but also tailored to your project's specific workflows and requirements.
Additional Resources
Exercises
- Create a custom admin site for an e-commerce application with products, orders, and customers.
- Implement a dashboard for your admin site that shows recent orders and popular products.
- Create a custom admin action that generates a CSV report of selected models.
- Customize the admin theme by overriding CSS and admin templates.
- Implement role-based permissions in the admin site using Django's permission system.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)