Introduction to the Django Admin
The Django Admin is one of Django's most powerful features—a fully-featured administrative interface that's automatically generated from your models. It allows site administrators to view, add, edit, and delete records without writing a single line of code.
Key features of the Django Admin include:
- Automatic CRUD operations for registered models
- Authentication and permission management
- Form validation and error handling
- Search and filtering functionality
- Many-to-many and foreign key relationship handling
- Customizable appearance and behavior
The Control Panel Analogy
Think of the Django Admin as the control panel of a modern building. Just as a building's control panel provides authorized personnel with access to manage various systems (HVAC, security, lighting) without understanding the underlying electrical and mechanical systems, the Django Admin provides authorized users with an interface to manage application data without writing code or SQL queries. The beauty is that this control panel is automatically generated based on your models, ready to use with minimal configuration.
Basic Admin Setup
To start using the Django Admin, you need to register your models with it. The simplest approach is to register a model directly:
1. Enable Admin in INSTALLED_APPS
Ensure that the admin app is enabled in your project's settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# ... your apps
]
2. Include Admin URLs
Make sure the admin URLs are included in your project's urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# ... your URLs
]
3. Register Models
Register your models in your app's admin.py file:
from django.contrib import admin
from .models import Article, Category, Tag
# Basic registration
admin.site.register(Article)
admin.site.register(Category)
admin.site.register(Tag)
This minimal setup provides a complete admin interface for your models. However, it often doesn't provide the best user experience for complex models. That's where customization comes in.
Creating Custom ModelAdmin Classes
To customize how a model appears in the admin, create a custom ModelAdmin class:
from django.contrib import admin
from .models import Article, Category, Tag
class ArticleAdmin(admin.ModelAdmin):
# Configure display of article list
list_display = ('title', 'author', 'status', 'category', 'created_at', 'published_at')
list_filter = ('status', 'category', 'created_at', 'author')
search_fields = ('title', 'content')
date_hierarchy = 'published_at'
ordering = ('-created_at',)
# Configure fields on the edit form
fieldsets = (
('Basic Information', {
'fields': ('title', 'slug', 'author', 'content')
}),
('Publishing Options', {
'fields': ('status', 'category', 'tags', 'published_at'),
'classes': ('collapse',)
}),
('Meta Information', {
'fields': ('meta_description', 'meta_keywords'),
'description': 'SEO metadata for the article'
})
)
# Prepopulate slug field from title
prepopulated_fields = {'slug': ('title',)}
# Add raw_id_fields for better handling of foreign keys with many objects
raw_id_fields = ('author',)
# Configure related objects
filter_horizontal = ('tags',)
# Register the model with the custom admin class
admin.site.register(Article, ArticleAdmin)
# Simpler models can use the @register decorator
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
The ModelAdmin class provides numerous options for customizing the admin interface. Let's explore these options in detail.
List View Customization
The list view is the first screen you see when you click on a model in the admin. You can customize it in several ways:
Display Fields and Formatting
class ArticleAdmin(admin.ModelAdmin):
# Basic list display
list_display = ('title', 'author', 'status', 'category', 'created_at')
# Add custom methods to list_display
list_display = ('title', 'author', 'status', 'category', 'created_at', 'is_recent')
def is_recent(self, obj):
"""Custom column that shows if the article is recent."""
from django.utils import timezone
return obj.created_at >= timezone.now() - timezone.timedelta(days=7)
# Configure the custom method
is_recent.short_description = 'Recent?' # Column header
is_recent.boolean = True # Display as icon
is_recent.admin_order_field = 'created_at' # Allow sorting
# Add clickable links to related models
list_display_links = ('title', 'slug')
# Add editable fields (edit without going to the detail page)
list_editable = ('status', 'category')
# Custom empty value display
empty_value_display = '-empty-'
Filtering and Searching
class ArticleAdmin(admin.ModelAdmin):
# Add filters in the right sidebar
list_filter = ('status', 'category', 'created_at', 'author')
# Add custom filters
list_filter = ('status', 'category', CategoryYearFilter, 'author')
# Add search functionality
search_fields = ('title', 'content')
# Add related field search
search_fields = ('title', 'content', 'author__username', 'author__email')
# Add date-based navigation
date_hierarchy = 'published_at'
Custom List Filters
from django.contrib.admin import SimpleListFilter
class CategoryYearFilter(SimpleListFilter):
"""Custom filter to filter by year and category."""
title = 'Year and Category'
parameter_name = 'year_category'
def lookups(self, request, model_admin):
"""Return a list of tuples (value, verbose name)."""
# Get all years with articles
years = Article.objects.dates('created_at', 'year')
# Get all categories
categories = Category.objects.all()
# Create tuples for each combination
lookups = []
for year in years:
year_str = str(year.year)
lookups.append((year_str, year_str))
for category in categories:
key = f"{year.year}-{category.id}"
value = f"{year.year} - {category.name}"
lookups.append((key, value))
return lookups
def queryset(self, request, queryset):
"""Filter the queryset based on the selected value."""
value = self.value()
if not value:
return queryset
# Check if value is just a year
if value.isdigit():
return queryset.filter(created_at__year=int(value))
# Value is year-category
year, category_id = value.split('-')
return queryset.filter(
created_at__year=int(year),
category_id=int(category_id)
)
Pagination and Ordering
class ArticleAdmin(admin.ModelAdmin):
# Set number of items per page
list_per_page = 25
# Default ordering
ordering = ('-created_at',)
# Show all fields in the changelist view
list_max_show_all = 1000 # Maximum allowed to show with 'show all'
# Preserve filters when navigating to detail view and back
preserve_filters = True
Detail View Customization
The detail view (or "change form") is where you edit an individual object. You can customize how fields are displayed and grouped:
Field Organization with Fieldsets
class ArticleAdmin(admin.ModelAdmin):
# Basic fieldsets
fieldsets = (
('Basic Information', {
'fields': ('title', 'slug', 'author', 'content')
}),
('Publishing Options', {
'fields': ('status', 'category', 'tags', 'published_at'),
'classes': ('collapse',) # Collapsed by default
}),
('Meta Information', {
'fields': ('meta_description', 'meta_keywords'),
'description': 'SEO metadata for the article'
})
)
# Exclude fields from the form
exclude = ('views_count', 'last_modified')
# Read-only fields
readonly_fields = ('created_at', 'updated_at')
# Automatically populate fields
prepopulated_fields = {'slug': ('title',)}
Inline Related Objects
Inlines allow you to edit related objects on the same page as the parent object:
# Define inline admin classes
class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # Number of empty forms to display
fields = ('author', 'content', 'approved')
readonly_fields = ('created_at',)
class ImageInline(admin.StackedInline):
model = ArticleImage
extra = 3
fields = ('image', 'caption', 'order')
class ArticleAdmin(admin.ModelAdmin):
# ... other options
# Add inlines to the detail view
inlines = [ImageInline, CommentInline]
There are two types of inlines:
TabularInline: Displays related objects in a table (more compact)StackedInline: Displays related objects as stacked forms (more space for fields)
Foreign Key Handling
class ArticleAdmin(admin.ModelAdmin):
# Use raw_id_fields for better handling of foreign keys with many objects
raw_id_fields = ('author',)
# Filter options for ForeignKey fields in forms
autocomplete_fields = ('category', 'tags')
# Many-to-many field display
filter_horizontal = ('tags',) # Horizontal selection widget
# Or
filter_vertical = ('tags',) # Vertical selection widget
Custom Actions
Actions allow you to perform operations on multiple objects at once. Django provides a "delete selected objects" action by default, but you can add your own:
class ArticleAdmin(admin.ModelAdmin):
# List of actions
actions = ['make_published', 'make_draft', 'export_as_csv']
def make_published(self, request, queryset):
"""Action to mark selected articles as published."""
updated = queryset.update(status='published')
self.message_user(
request,
f'{updated} article{"s" if updated != 1 else ""} were marked as published.'
)
make_published.short_description = 'Mark selected articles as published'
def make_draft(self, request, queryset):
"""Action to mark selected articles as draft."""
updated = queryset.update(status='draft')
self.message_user(
request,
f'{updated} article{"s" if updated != 1 else ""} were marked as draft.'
)
make_draft.short_description = 'Mark selected articles as draft'
def export_as_csv(self, request, queryset):
"""Action to export selected articles as CSV."""
import csv
from django.http import HttpResponse
# Create a response with CSV header
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=articles.csv'
# Create CSV writer
writer = csv.writer(response)
# Write headers
writer.writerow(['Title', 'Author', 'Status', 'Category', 'Created At'])
# Write data
for article in queryset:
writer.writerow([
article.title,
article.author.username,
article.status,
article.category.name if article.category else 'None',
article.created_at.strftime('%Y-%m-%d')
])
return response
export_as_csv.short_description = 'Export selected articles as CSV'
Action Permissions
You can restrict who can use an action:
class ArticleAdmin(admin.ModelAdmin):
# ... other options and actions
def has_make_published_permission(self, request):
"""Check if the user has permission to publish articles."""
return request.user.has_perm('blog.can_publish')
def has_export_as_csv_permission(self, request):
"""Check if the user has permission to export articles."""
return request.user.is_superuser
Admin Display Hooks
You can customize how model instances are displayed in the admin:
class Article(models.Model):
# ... fields
def __str__(self):
"""String representation of the article."""
return self.title
def get_absolute_url(self):
"""Get the URL of the article."""
from django.urls import reverse
return reverse('blog:article_detail', kwargs={'slug': self.slug})
class Meta:
verbose_name = 'Article'
verbose_name_plural = 'Articles'
And in your ModelAdmin class:
class ArticleAdmin(admin.ModelAdmin):
# ... other options
# Show a "View on site" button
view_on_site = True # Uses get_absolute_url
# Or define a custom view_on_site method
def view_on_site(self, obj):
return f"/blog/{obj.slug}/"
Admin Form Customization
You can customize the forms used in the admin interface:
Custom Form Classes
from django import forms
from .models import Article
class ArticleAdminForm(forms.ModelForm):
"""Custom form for Article admin."""
# Add a custom field
notes = forms.CharField(
widget=forms.Textarea,
required=False,
help_text='Notes for editors (not published)'
)
# Add additional validation
def clean_title(self):
"""Validate the title field."""
title = self.cleaned_data['title']
if 'banned word' in title.lower():
raise forms.ValidationError('Title contains banned words')
return title
# Configure the form
class Meta:
model = Article
fields = '__all__'
widgets = {
'content': forms.Textarea(attrs={'rows': 30, 'cols': 80}),
'meta_description': forms.Textarea(attrs={'rows': 3}),
}
class ArticleAdmin(admin.ModelAdmin):
# Use the custom form
form = ArticleAdminForm
Custom Save Methods
class ArticleAdmin(admin.ModelAdmin):
# ... other options
def save_model(self, request, obj, form, change):
"""Custom save method."""
# Set the author if not set
if not obj.author:
obj.author = request.user
# Log the change
if change:
self.log_change(request, obj, 'Changed by admin')
else:
self.log_addition(request, obj, 'Added by admin')
# Call the parent method to save
super().save_model(request, obj, form, change)
def save_formset(self, request, form, formset, change):
"""Custom save method for inline formsets."""
instances = formset.save(commit=False)
# Process each instance in the formset
for instance in instances:
# Set attributes if needed
if not instance.created_by:
instance.created_by = request.user
# Save the instance
instance.save()
# Call delete() on objects to be deleted
for obj in formset.deleted_objects:
obj.delete()
# Call the parent method
super().save_formset(request, form, formset, change)
Admin Templates Customization
Django allows you to override admin templates to change its appearance and behavior:
Template Hierarchy
Django looks for admin templates in the following order:
app_label/admin/model_name/action.htmlapp_label/admin/action.htmladmin/model_name/action.htmladmin/action.html
For example, to customize the change form for the Article model in the blog app:
blog/admin/article/change_form.htmlblog/admin/change_form.htmladmin/article/change_form.htmladmin/change_form.html
Example: Customizing the Change Form
Create a file at templates/admin/blog/article/change_form.html:
{% extends "admin/change_form.html" %}
{% load static %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/article_admin.css' %}">
<script src="{% static 'admin/js/article_admin.js' %}"></script>
{% endblock %}
{% block form_top %}
<div class="article-preview-warning">
<p>Remember to preview articles before publishing!</p>
{% if original %}
<a href="{{ original.get_absolute_url }}" target="_blank" class="button">Preview</a>
{% endif %}
</div>
{% endblock %}
{% block submit_buttons_bottom %}
{{ block.super }}
<div class="submit-row">
<input type="submit" value="Save as Draft" name="_save_as_draft" class="default" />
</div>
{% endblock %}
Then handle the custom submit button in the ArticleAdmin class:
class ArticleAdmin(admin.ModelAdmin):
# ... other options
def save_model(self, request, obj, form, change):
"""Custom save method that handles the custom submit button."""
if '_save_as_draft' in request.POST:
obj.status = 'draft'
super().save_model(request, obj, form, change)
Example: Custom Admin Index
Create a file at templates/admin/index.html:
{% extends "admin/index.html" %}
{% load static %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/custom_dashboard.css' %}">
{% endblock %}
{% block content %}
<div class="admin-dashboard-header">
<h1>Welcome to {{ site_title }}</h1>
<p>Today is {{ current_date|date }} and you are logged in as {{ user.username }}.</p>
<div class="dashboard-stats">
<div class="stat-box">
<h3>Articles</h3>
<p class="stat-number">{{ article_count }}</p>
</div>
<div class="stat-box">
<h3>Users</h3>
<p class="stat-number">{{ user_count }}</p>
</div>
<div class="stat-box">
<h3>Comments</h3>
<p class="stat-number">{{ comment_count }}</p>
</div>
</div>
</div>
{{ block.super }}
{% endblock %}
Then provide the context variables in your custom admin site class (see next section).
Customizing the Admin Site
You can create a custom AdminSite class to completely customize the admin interface:
from django.contrib.admin import AdminSite
from django.utils import timezone
class BlogAdminSite(AdminSite):
# Customize site attributes
site_header = 'Blog Administration'
site_title = 'Blog Admin Portal'
index_title = 'Blog Management'
# Custom view for the index page
def index(self, request, extra_context=None):
"""Customize the admin index page."""
from .models import Article, Comment
from django.contrib.auth.models import User
# Add extra context
if extra_context is None:
extra_context = {}
extra_context.update({
'current_date': timezone.now(),
'article_count': Article.objects.count(),
'user_count': User.objects.count(),
'comment_count': Comment.objects.count(),
})
return super().index(request, extra_context)
# Custom authentication method
def has_permission(self, request):
"""Check if the user has permission to access the admin site."""
if not super().has_permission(request):
return False
# Only allow staff with specific permissions
return request.user.has_perm('blog.access_admin')
# Create an instance of the custom admin site
blog_admin_site = BlogAdminSite(name='blog_admin')
# Register models with the custom admin site
blog_admin_site.register(Article, ArticleAdmin)
blog_admin_site.register(Category, CategoryAdmin)
blog_admin_site.register(Tag, TagAdmin)
Then update your project's urls.py to use the custom admin site:
from django.urls import path
from blog.admin import blog_admin_site
urlpatterns = [
path('admin/', blog_admin_site.urls),
# ... other URLs
]
Multiple Admin Sites
You can create multiple admin sites for different purposes:
# Create different admin sites
admin_site = AdminSite(name='admin')
staff_admin_site = AdminSite(name='staff_admin')
staff_admin_site.site_header = 'Staff Administration'
# Register models with different admin sites
admin_site.register(User, UserAdmin)
admin_site.register(Article, ArticleAdmin)
admin_site.register(Category, CategoryAdmin)
# Staff can only manage content
staff_admin_site.register(Article, StaffArticleAdmin)
staff_admin_site.register(Category, CategoryAdmin)
# In urls.py
urlpatterns = [
path('admin/', admin_site.urls),
path('staff-admin/', staff_admin_site.urls),
]
Admin Interface Styling
You can customize the look and feel of the admin interface with CSS and JavaScript:
Adding CSS and JavaScript
Create a file templates/admin/base_site.html:
{% extends "admin/base_site.html" %}
{% load static %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/custom_admin.css' %}">
{% endblock %}
{% block extrahead %}
{{ block.super }}
<script src="{% static 'admin/js/custom_admin.js' %}"></script>
{% endblock %}
Create the CSS file at static/admin/css/custom_admin.css:
/* Custom Admin CSS */
/* Change header color */
#header {
background: #2c3e50;
color: #ecf0f1;
}
/* Style the sidebar */
.module h2, .module caption, .inline-group h2 {
background: #34495e;
}
/* Style buttons */
.button, input[type=submit], input[type=button], .submit-row input, a.button {
background: #2980b9;
}
/* Style primary buttons */
.button.default, input[type=submit].default, .submit-row input.default {
background: #27ae60;
}
/* Add custom styling for specific admin pages */
body.app-blog.model-article .submit-row {
background: #f9f9f9;
border-top: 1px solid #eee;
}
/* Style list view */
#changelist table thead th {
background: #3498db;
color: #fff;
}
/* Style selected row */
#changelist .selected {
background-color: #e74c3c;
}
/* Custom dashboard stats */
.dashboard-stats {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.stat-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
width: 30%;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #2980b9;
}
Create the JavaScript file at static/admin/js/custom_admin.js:
// Custom Admin JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Add confirmation for publishing articles
const publishButtons = document.querySelectorAll('input[name="_publish"]');
publishButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm('Are you sure you want to publish this article?')) {
e.preventDefault();
}
});
});
// Add WYSIWYG editor to content field
if (document.getElementById('id_content')) {
// Initialize a rich text editor (example)
// Note: This requires a WYSIWYG library to be included
// For example: ClassicEditor.create(document.getElementById('id_content'));
}
// Add date picker to date fields
const dateFields = document.querySelectorAll('.vDateField');
dateFields.forEach(field => {
// Add date picker functionality
// For example: flatpickr(field, { dateFormat: 'Y-m-d' });
});
});
Real-World Example: Complete Blog Admin
Let's put everything together in a comprehensive example for a blog application:
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.db.models import Count
from .models import Article, Category, Tag, Comment, Image
# Register the admin site
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'article_count')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
def article_count(self, obj):
"""Count articles in this category."""
return obj.articles.count()
article_count.short_description = 'Articles'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'article_count')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
def article_count(self, obj):
"""Count articles with this tag."""
return obj.articles.count()
article_count.short_description = 'Articles'
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
fields = ('author', 'content', 'approved', 'created_at')
readonly_fields = ('created_at',)
def has_add_permission(self, request, obj=None):
return False # Prevent adding comments through the admin
class ImageInline(admin.TabularInline):
model = Image
extra = 1
fields = ('image', 'caption', 'order', 'display_image')
readonly_fields = ('display_image',)
def display_image(self, obj):
"""Display a thumbnail of the image."""
if obj.image:
return format_html(
'<img src="{}" width="100" height="100" style="object-fit: cover;" />',
obj.image.url
)
return 'No Image'
display_image.short_description = 'Thumbnail'
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
# List view options
list_display = ('title', 'author', 'category', 'status', 'view_count', 'tag_list', 'created_at', 'admin_actions')
list_filter = ('status', 'category', 'tags', 'created_at')
search_fields = ('title', 'content', 'author__username', 'author__email')
date_hierarchy = 'created_at'
list_per_page = 20
# Form options
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'author', 'content', 'excerpt'),
}),
('Publishing', {
'fields': ('status', 'category', 'tags', 'featured_image', 'published_at'),
'classes': ('collapse',),
}),
('Statistics', {
'fields': ('view_count', 'created_at', 'updated_at'),
'classes': ('collapse',),
}),
('SEO', {
'fields': ('meta_title', 'meta_description', 'meta_keywords'),
'classes': ('collapse',),
'description': 'Search engine optimization fields',
}),
)
readonly_fields = ('created_at', 'updated_at', 'view_count')
prepopulated_fields = {'slug': ('title',)}
filter_horizontal = ('tags',)
raw_id_fields = ('author',)
inlines = [ImageInline, CommentInline]
# Custom actions
actions = ['make_published', 'make_draft', 'export_as_csv']
def get_queryset(self, request):
"""Optimize the queryset for the admin list view."""
queryset = super().get_queryset(request)
queryset = queryset.select_related('author', 'category')
queryset = queryset.prefetch_related('tags')
return queryset
def tag_list(self, obj):
"""Display a list of tags as links."""
return format_html(
', '.join(
f'<a href="{reverse("admin:blog_tag_change", args=[tag.pk])}">{tag.name}</a>'
for tag in obj.tags.all()
)
)
tag_list.short_description = 'Tags'
def admin_actions(self, obj):
"""Display action buttons."""
if obj.status == 'draft':
return format_html(
'<a class="button" href="{}">Publish</a>',
reverse('admin:publish_article', args=[obj.pk])
)
return format_html(
'<a class="button" href="{}">View</a>',
obj.get_absolute_url()
)
admin_actions.short_description = 'Actions'
def make_published(self, request, queryset):
"""Mark selected articles as published."""
updated = queryset.update(status='published')
self.message_user(
request,
f'{updated} article{"s" if updated != 1 else ""} marked as published.'
)
make_published.short_description = 'Mark selected articles as published'
def make_draft(self, request, queryset):
"""Mark selected articles as draft."""
updated = queryset.update(status='draft')
self.message_user(
request,
f'{updated} article{"s" if updated != 1 else ""} marked as draft.'
)
make_draft.short_description = 'Mark selected articles as draft'
def export_as_csv(self, request, queryset):
"""Export selected articles as CSV."""
import csv
from django.http import HttpResponse
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=articles.csv'
writer = csv.writer(response)
writer.writerow(['Title', 'Author', 'Category', 'Status', 'Created At', 'Tags'])
for article in queryset:
writer.writerow([
article.title,
article.author.username,
article.category.name if article.category else '',
article.status,
article.created_at.strftime('%Y-%m-%d'),
', '.join(tag.name for tag in article.tags.all())
])
return response
export_as_csv.short_description = 'Export selected articles as CSV'
def get_urls(self):
"""Add custom URLs to the admin."""
from django.urls import path
urls = super().get_urls()
custom_urls = [
path(
'/publish/',
self.admin_site.admin_view(self.publish_article),
name='publish_article',
),
]
return custom_urls + urls
def publish_article(self, request, article_id):
"""Custom view to publish an article."""
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
from django.utils import timezone
article = get_object_or_404(Article, id=article_id)
article.status = 'published'
article.published_at = timezone.now()
article.save()
messages.success(request, f'Article "{article.title}" has been published.')
return redirect('admin:blog_article_changelist')
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('author', 'article', 'created_at', 'approved', 'content_excerpt')
list_filter = ('approved', 'created_at')
search_fields = ('author__username', 'content', 'article__title')
date_hierarchy = 'created_at'
readonly_fields = ('created_at',)
actions = ['approve_comments', 'reject_comments']
def content_excerpt(self, obj):
"""Display an excerpt of the comment content."""
if len(obj.content) > 50:
return obj.content[:50] + '...'
return obj.content
content_excerpt.short_description = 'Content'
def approve_comments(self, request, queryset):
"""Approve selected comments."""
updated = queryset.update(approved=True)
self.message_user(
request,
f'{updated} comment{"s" if updated != 1 else ""} approved.'
)
approve_comments.short_description = 'Approve selected comments'
def reject_comments(self, request, queryset):
"""Reject selected comments."""
updated = queryset.update(approved=False)
self.message_user(
request,
f'{updated} comment{"s" if updated != 1 else ""} rejected.'
)
reject_comments.short_description = 'Reject selected comments'
# Customize the admin site
admin.site.site_header = 'Blog Administration'
admin.site.site_title = 'Blog Admin'
admin.site.index_title = 'Blog Management'
This comprehensive example includes:
- Customized list views for all models
- Inline editing for related models
- Custom actions for bulk operations
- Custom admin views for specific operations
- Optimized querysets for performance
- Custom display methods for better visualization
- Fieldsets for organized form display
- Admin site customization
Best Practices for Admin Customization
Here are some best practices to follow when customizing the Django Admin:
Organization
- Group related fields: Use fieldsets to organize related fields
- Use meaningful names: Provide descriptive names for fields and actions
- Plan your hierarchy: Consider how models relate to each other
- Optimize performance: Use select_related and prefetch_related to reduce database queries
Security
- Restrict access: Use permissions to limit who can access what
- Validate user input: Add validation to forms and clean methods
- Log actions: Enable admin logging to track changes
- Be cautious with custom views: Ensure they check permissions
Usability
- Add search and filtering: Make it easy to find records
- Use appropriate widgets: Choose the right form controls for each field
- Add help text: Provide guidance for complex fields
- Create useful actions: Add bulk actions for common operations
- Be consistent: Use similar patterns across all admin interfaces
Maintenance
- Document your customizations: Add comments to explain complex code
- Test thoroughly: Ensure all admin features work as expected
- Keep it simple: Avoid overly complex customizations when possible
- Separate concerns: Keep admin code in admin.py, not mixed with models
Practice Activity
Create a custom admin interface for an e-commerce application with the following models:
- Product: name, description, price, SKU, category, tags, images
- Category: name, slug, parent category, description
- Tag: name, slug
- Order: customer, status, created_at, items, total_price
- OrderItem: order, product, quantity, price
- Customer: user, shipping_address, billing_address, phone
Your admin customization should include:
- Custom ModelAdmin classes for each model with appropriate list displays, filters, and search fields
- Custom actions for common operations (e.g., mark orders as shipped, mark products as featured)
- Inline editing for related models (e.g., order items in order)
- Custom display methods (e.g., thumbnail display for product images)
- Fieldsets to organize forms
- Custom list filters (e.g., filter products by price range)
- Admin site customization (site header, title, etc.)
Bonus challenges:
- Add a custom view to generate order invoices as PDFs
- Create a dashboard with sales statistics
- Implement a custom form for bulk product import from CSV
- Add a custom admin template for the product detail view
Further Topics to Explore
- Admin actions with intermediate pages
- Integration with REST APIs
- Admin theming frameworks
- Dashboard plugins and extensions
- Report generation in the admin
- File management and batch uploads
- Admin customization with third-party packages
- Graph and chart integration in the admin
- Admin for mobile devices
- Multi-database admin