Advanced Django Admin: Inlines, Actions, and Custom Admin Views

Advanced Django admin features extend basic administration capabilities with inline editing for related models, custom actions for bulk operations, custom views for specialized workflows, and admin filters for complex queries. Inline editing enables managing related objects within parent object's admin page streamlining workflows. Custom actions perform bulk operations on multiple selected objects. Custom admin views add specialized functionality beyond standard CRUD operations. Mastering advanced admin features transforms the admin interface into a powerful application management tool supporting complex workflows, batch operations, and specialized administrative tasks from content approval workflows to report generation and data import/export capabilities providing comprehensive administrative functionality.
Inline Editing
Inline editing allows managing related objects directly within parent object's admin page using TabularInline for compact table layouts or StackedInline for full field display. Inlines streamline editing workflows eliminating navigation between separate admin pages. Understanding inlines enables efficient management of parent-child relationships.
# Inline Editing
from django.contrib import admin
from .models import Post, Comment, Image
# Tabular inline (table format)
class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # Number of empty forms
fields = ['author', 'text', 'approved']
readonly_fields = ['created_at']
# Stacked inline (full form format)
class ImageInline(admin.StackedInline):
model = Image
extra = 0
max_num = 5 # Maximum number of inlines
min_num = 1 # Minimum number required
fields = ['image', 'caption', 'order']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline, ImageInline]
list_display = ['title', 'author', 'published_date']
# Advanced inline configuration
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
can_delete = True # Allow deletion
show_change_link = True # Link to full edit page
# Customize queryset
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(approved=True)
# Conditional readonly fields
def get_readonly_fields(self, request, obj=None):
if obj and obj.is_published:
return ['author', 'text']
return []
# Inline with custom form
from django import forms
class CommentInlineForm(forms.ModelForm):
class Meta:
model = Comment
fields = '__all__'
def clean_text(self):
text = self.cleaned_data['text']
if len(text) < 10:
raise forms.ValidationError('Comment too short')
return text
class CommentInline(admin.TabularInline):
model = Comment
form = CommentInlineForm
# Generic inline (for generic relations)
from django.contrib.contenttypes.admin import GenericTabularInline
from .models import Tag
class TagInline(GenericTabularInline):
model = Tag
extra = 1Custom Admin Actions
Custom admin actions perform operations on multiple selected objects through dropdown menu in list view. Actions enable bulk operations like publishing posts, deleting records, or exporting data. Understanding actions enables efficient batch processing and workflow automation.
# Custom Admin Actions
from django.contrib import admin
from django.utils import timezone
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'status', 'published_date']
actions = ['make_published', 'make_draft', 'export_as_csv']
# Action method
@admin.action(description='Publish selected posts')
def make_published(self, request, queryset):
updated = queryset.update(
status='published',
published_date=timezone.now()
)
self.message_user(request, f'{updated} posts published successfully.')
@admin.action(description='Move to draft')
def make_draft(self, request, queryset):
queryset.update(status='draft')
self.message_user(request, 'Selected posts moved to draft.')
# Action with confirmation
@admin.action(description='Delete with confirmation')
def delete_with_warning(self, request, queryset):
count = queryset.count()
if count > 10:
self.message_user(
request,
f'Cannot delete more than 10 posts at once',
level='error'
)
return
queryset.delete()
self.message_user(request, f'{count} posts deleted')
# Export action
import csv
from django.http import HttpResponse
@admin.action(description='Export to CSV')
def export_as_csv(self, request, queryset):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
writer = csv.writer(response)
writer.writerow(['ID', 'Title', 'Status', 'Author', 'Views'])
for post in queryset:
writer.writerow([
post.id,
post.title,
post.status,
post.author.username,
post.views
])
return response
# Action with intermediate page
from django.shortcuts import render
@admin.action(description='Send notification')
def send_notification(modeladmin, request, queryset):
if 'apply' in request.POST:
message = request.POST.get('message')
for post in queryset:
# Send notification
send_email(post.author.email, message)
modeladmin.message_user(request, f'Sent to {queryset.count()} authors')
return
return render(request, 'admin/send_notification.html', {
'posts': queryset,
'action': 'send_notification'
})
# Disable default delete action
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
def get_actions(self, request):
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actionsCustom Admin Views
Custom admin views add specialized functionality beyond standard CRUD operations enabling reports, dashboards, import/export interfaces, and custom workflows. Custom views integrate with admin interface providing consistent user experience. Understanding custom views enables extending admin with application-specific functionality.
# Custom Admin Views
from django.contrib import admin
from django.urls import path
from django.shortcuts import render
from django.http import HttpResponse
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
change_list_template = 'admin/post_changelist.html'
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('statistics/', self.admin_site.admin_view(self.statistics_view), name='post_statistics'),
path('import/', self.admin_site.admin_view(self.import_view), name='post_import'),
path('export/', self.admin_site.admin_view(self.export_view), name='post_export'),
]
return custom_urls + urls
def statistics_view(self, request):
from django.db.models import Count, Avg, Sum
stats = {
'total_posts': Post.objects.count(),
'published': Post.objects.filter(status='published').count(),
'drafts': Post.objects.filter(status='draft').count(),
'avg_views': Post.objects.aggregate(Avg('views'))['views__avg'],
'total_views': Post.objects.aggregate(Sum('views'))['views__sum'],
'by_author': Post.objects.values('author__username').annotate(count=Count('id')),
}
context = {
**self.admin_site.each_context(request),
'title': 'Post Statistics',
'stats': stats,
}
return render(request, 'admin/post_statistics.html', context)
def import_view(self, request):
if request.method == 'POST':
csv_file = request.FILES['file']
# Process CSV import
import csv
reader = csv.reader(csv_file.read().decode('utf-8').splitlines())
next(reader) # Skip header
imported = 0
for row in reader:
Post.objects.create(
title=row[0],
content=row[1],
author_id=row[2]
)
imported += 1
self.message_user(request, f'Imported {imported} posts')
return redirect('..')
context = {
**self.admin_site.each_context(request),
'title': 'Import Posts',
}
return render(request, 'admin/post_import.html', context)
def export_view(self, request):
import csv
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts_export.csv"'
writer = csv.writer(response)
writer.writerow(['ID', 'Title', 'Content', 'Author', 'Status', 'Views'])
for post in Post.objects.all():
writer.writerow([
post.id,
post.title,
post.content,
post.author.username,
post.status,
post.views
])
return response
# Custom dashboard
class MyAdminSite(admin.AdminSite):
site_header = 'My Admin'
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('dashboard/', self.admin_view(self.dashboard_view), name='dashboard'),
]
return custom_urls + urls
def dashboard_view(self, request):
context = {
**self.each_context(request),
'title': 'Dashboard',
'recent_posts': Post.objects.order_by('-created_at')[:10],
}
return render(request, 'admin/dashboard.html', context)
# Template: admin/post_changelist.html
# {% extends "admin/change_list.html" %}
# {% block object-tools-items %}
# {{ block.super }}
# <li>
# <a href="{% url 'admin:post_statistics' %}" class="button">Statistics</a>
# </li>
# <li>
# <a href="{% url 'admin:post_import' %}" class="button">Import</a>
# </li>
# <li>
# <a href="{% url 'admin:post_export' %}" class="button">Export</a>
# </li>
# {% endblock %}Advanced Admin Best Practices
Effective advanced admin usage follows patterns ensuring robust functionality. Use inlines for tightly related objects keeping workflows efficient. Implement custom actions for common bulk operations with proper validation. Add custom views for specialized functionality maintaining admin interface consistency. Optimize inline queries preventing performance issues. Provide clear action descriptions and confirmations. Handle errors gracefully with informative messages. Test custom functionality thoroughly ensuring proper permissions. Document complex customizations for maintainers. These practices ensure professional admin interfaces supporting complex workflows and batch operations efficiently.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


