Chapter 10: Django Admin
10.2 Advanced Admin Configuration
Custom List Display
Advanced list display configuration can significantly improve the usability of the admin backend:
python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Article, Category, Tag
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
# Basic list configuration
list_display = [
'title', 'author', 'category', 'is_published',
'created_at', 'view_count', 'status_indicator'
]
# Number of items per page in list
list_per_page = 25
# Editable fields in list
list_editable = ['is_published', 'category']
# Sortable fields
sortable_by = ['title', 'created_at', 'author']
# Custom method fields
def view_count(self, obj):
"""Show article view count"""
return obj.view_count or 0
view_count.short_description = 'Views'
view_count.admin_order_field = 'view_count'
def status_indicator(self, obj):
"""Show status indicator"""
if obj.is_published:
color = '#28a745' # Green
text = 'Published'
else:
color = '#ffc107' # Yellow
text = 'Draft'
return format_html(
'<span style="background-color: {}; color: white; '
'padding: 4px 8px; border-radius: 4px; font-size: 12px;">{}</span>',
color, text
)
status_indicator.short_description = 'Status'
# Fields with links
def title_link(self, obj):
"""Title with edit link"""
url = reverse('admin:myapp_article_change', args=[obj.pk])
return format_html('<a href="{}">{}</a>', url, obj.title)
title_link.short_description = 'Title'
title_link.admin_order_field = 'title'
# Conditional field display
def get_list_display(self, request):
"""Dynamically adjust displayed fields based on user permissions"""
if request.user.is_superuser:
return self.list_display + ['edit_link']
return self.list_display
def edit_link(self, obj):
"""Edit link"""
url = reverse('admin:myapp_article_change', args=[obj.pk])
return format_html('<a href="{}">Edit</a>', url)
edit_link.short_description = 'Action'
# Custom list filters
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
class PublishedFilter(admin.SimpleListFilter):
title = _('Publish Status')
parameter_name = 'published'
def lookups(self, request, model_admin):
return (
('published', _('Published')),
('draft', _('Draft')),
('recent', _('Recently Published')),
)
def queryset(self, request, queryset):
if self.value() == 'published':
return queryset.filter(is_published=True)
if self.value() == 'draft':
return queryset.filter(is_published=False)
if self.value() == 'recent':
from django.utils import timezone
from datetime import timedelta
recent_date = timezone.now() - timedelta(days=7)
return queryset.filter(
is_published=True,
created_at__gte=recent_date
)
return queryset
# Use custom filter in ArticleAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_filter = [
'category', 'tags', 'is_published', 'created_at',
PublishedFilter, 'author'
]Filters and Search
Configure advanced filters and search functions:
python
# admin.py
from django.contrib import admin
from django.db import models
from django.forms import TextInput, Textarea
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
# Search fields
search_fields = [
'title', 'content', 'excerpt',
'author__username', 'author__first_name', 'author__last_name',
'category__name', 'tags__name'
]
# Search field configuration
search_help_text = 'Search title, content, author, category or tags'
# List filters
list_filter = [
'is_published',
'category',
'tags',
'created_at',
'author',
'updated_at'
]
# Date hierarchy filter
date_hierarchy = 'created_at'
# Custom search function
def get_search_results(self, request, queryset, search_term):
"""Custom search logic"""
queryset, use_distinct = super().get_search_results(
request, queryset, search_term
)
# Add custom search logic
if search_term:
# Search articles with related tags
queryset |= self.model.objects.filter(
tags__name__icontains=search_term
)
# Search author names
queryset |= self.model.objects.filter(
author__first_name__icontains=search_term
) | self.model.objects.filter(
author__last_name__icontains=search_term
)
return queryset, True
# Custom filter
class WordCountFilter(admin.SimpleListFilter):
title = 'Word Count Range'
parameter_name = 'word_count'
def lookups(self, request, model_admin):
return (
('short', 'Short articles (< 500 words)'),
('medium', 'Medium articles (500-2000 words)'),
('long', 'Long articles (> 2000 words)'),
)
def queryset(self, request, queryset):
if self.value() == 'short':
return queryset.filter(content__length__lt=500)
if self.value() == 'medium':
return queryset.filter(
content__length__gte=500,
content__length__lt=2000
)
if self.value() == 'long':
return queryset.filter(content__length__gte=2000)
return queryset
# Use in filters
list_filter = [
'is_published',
'category',
WordCountFilter, # Custom filter
'created_at'
]Inline Editing
Inline editing allows editing related models on the same page:
python
# models.py
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
author_name = models.CharField(max_length=100)
email = models.EmailField()
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
is_approved = models.BooleanField(default=False)
# admin.py
from django.contrib import admin
from .models import Article, Comment
# Basic inline editing
class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # Extra empty rows to display
fields = ['author_name', 'email', 'content', 'is_approved', 'created_at']
readonly_fields = ['created_at']
# Stacked inline editing
class CommentStackedInline(admin.StackedInline):
model = Comment
extra = 0
fields = ['author_name', 'email', 'content', 'is_approved']
readonly_fields = ['created_at']
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
inlines = [CommentInline] # Use inline editing
list_display = ['title', 'author', 'comment_count']
def comment_count(self, obj):
return obj.comment_set.count()
comment_count.short_description = 'Comment Count'
# More complex inline editing
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
fields = [
'author_name', 'email', 'content', 'is_approved',
'created_at', 'approve_button'
]
readonly_fields = ['created_at', 'approve_button']
def approve_button(self, obj):
"""Approve button"""
if obj.pk and not obj.is_approved:
url = reverse('admin:approve_comment', args=[obj.pk])
return format_html(
'<a class="button" href="{}">Approve</a>', url
)
elif obj.is_approved:
return 'Approved'
return ''
approve_button.short_description = 'Action'
# Limit number of inline objects displayed
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('article')
# Custom inline editing behavior
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
# Custom form
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
# Modify form based on user permissions
if not request.user.is_superuser:
# Non-superusers cannot modify approved comments
if 'is_approved' in formset.form.base_fields:
formset.form.base_fields['is_approved'].widget.attrs['disabled'] = True
return formset
# Custom save behavior
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
# Automatically set article author to current user (if not set)
if not instance.article.author_id:
instance.article.author = request.user
instance.article.save()
instance.save()
formset.save_m2m()Custom Admin Actions
Create custom admin actions to batch process objects:
python
# admin.py
from django.contrib import admin
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from .models import Article
def make_published(modeladmin, request, queryset):
"""Publish selected articles"""
updated = queryset.update(is_published=True)
modeladmin.message_user(
request,
f'Successfully published {updated} articles.',
messages.SUCCESS
)
make_published.short_description = "Publish selected articles"
def make_draft(modeladmin, request, queryset):
"""Set selected articles as draft"""
updated = queryset.update(is_published=False)
modeladmin.message_user(
request,
f'Successfully set {updated} articles as draft.',
messages.INFO
)
make_draft.short_description = "Set selected articles as draft"
def duplicate_articles(modeladmin, request, queryset):
"""Duplicate selected articles"""
for article in queryset:
# Create article copy
article.pk = None
article.title = f"{article.title} (Copy)"
article.slug = f"{article.slug}-copy"
article.is_published = False
article.save()
modeladmin.message_user(
request,
f'Successfully duplicated {queryset.count()} articles.',
messages.SUCCESS
)
duplicate_articles.short_description = "Duplicate selected articles"
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'is_published', 'created_at']
actions = [make_published, make_draft, duplicate_articles]
# Custom action permissions
def get_actions(self, request):
actions = super().get_actions(request)
# Non-superusers cannot use duplicate function
if not request.user.is_superuser:
if 'duplicate_articles' in actions:
del actions['duplicate_articles']
return actions
# Conditional actions
def get_action_choices(self, request, default_choices=[]):
"""Dynamically adjust action options based on conditions"""
choices = super().get_action_choices(request, default_choices)
if not request.user.is_superuser:
# Remove certain actions
choices = [choice for choice in choices if choice[0] != 'duplicate_articles']
return choices
# More complex custom actions
def export_selected_articles(modeladmin, request, queryset):
"""Export selected articles"""
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', 'Publish Time', 'Content'])
for article in queryset:
writer.writerow([
article.title,
article.author.username,
article.category.name if article.category else '',
article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
article.content[:100] + '...' if len(article.content) > 100 else article.content
])
return response
export_selected_articles.short_description = "Export selected articles as CSV"
# Actions with confirmation step
def delete_selected_articles(modeladmin, request, queryset):
"""Delete selected articles (with confirmation)"""
if 'apply' in request.POST:
# Execute deletion
count = queryset.count()
queryset.delete()
modeladmin.message_user(
request,
f'Successfully deleted {count} articles.',
messages.WARNING
)
return HttpResponseRedirect(request.get_full_path())
# Show confirmation page
context = {
'articles': queryset,
'action_checkbox_name': admin.ACTION_CHECKBOX_NAME,
}
return render(request, 'admin/confirm_delete_articles.html', context)
delete_selected_articles.short_description = "Delete selected articles"
# Use in ArticleAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'is_published', 'created_at']
actions = [
make_published,
make_draft,
duplicate_articles,
export_selected_articles,
delete_selected_articles
]With these advanced configurations, you can create a powerful Django admin backend with good user experience.
Summary
Django advanced admin configuration provides rich customization options:
- ✅ Custom list display and filters improve management efficiency
- ✅ Inline editing simplifies related model operations
- ✅ Custom admin actions support batch processing
- ✅ Permission control ensures operation security
- ✅ Search and filtering enhance data lookup
Mastering advanced configuration skills can build professional admin backends.
Next Article
We will learn about static file and media file handling.