Module 6: Django Admin Mastery
🎯 What You Will Learn in This Module
By the end of this tutorial, you will:
- Understand the power of Django's built-in admin panel
- Customize list views with columns, filters, and search
- Use inlines to edit related models on the same page
- Organize forms with fieldsets for better UX
- Create custom admin actions for bulk operations
- Optimize admin queries with
select_relatedto prevent N+1 issues - Export data to CSV directly from the admin panel
- Build a professional backoffice for your blog
📖 1. What is the Django Admin?
Django Admin is a built-in backoffice interface that reads metadata from your models to provide a quick, content-centric interface where trusted users can manage content on your site.
What it gives you for free:
- Create, read, update, and delete (CRUD) any model
- Search, filter, and sort records
- User and group management
- Automatic form generation with validation
What you will learn to customize:
- Which columns appear in list views
- Filters, search fields, and date hierarchies
- Inline editing for related models
- Custom bulk actions (like "Export to CSV")
- Form layout with fieldsets
- Query optimization
🏗️ 2. Registering Models in Admin
By default, your models don't appear in the admin. You must register them.
Basic Registration
1# blog/admin.py 2 3from django.contrib import admin 4from .models import Post, Category, Comment 5 6admin.site.register(Post) 7admin.site.register(Category) 8admin.site.register(Comment)
This works, but it gives you zero customization. For a professional interface, you use ModelAdmin classes.
📝 3. Customizing the List View
The list view is the first screen you see when clicking a model in the admin. Let's make it informative.
1# blog/admin.py 2 3from django.contrib import admin 4from django.db.models import Count 5from .models import Category, Post, Comment 6 7 8@admin.register(Category) 9class CategoryAdmin(admin.ModelAdmin): 10 """ 11 Custom admin interface for the Category model. 12 """ 13 # Columns displayed in the list view 14 list_display = ['name', 'post_count', 'is_active'] 15 16 # Filters in the right sidebar 17 list_filter = ['is_active'] 18 19 # Searchable fields 20 search_fields = ['name', 'description'] 21 22 # Auto-generate slug from name when typing 23 prepopulated_fields = {'slug': ('name',)} 24 25 def post_count(self, obj): 26 """ 27 Custom column showing how many posts are in this category. 28 """ 29 return obj.posts.count() 30 31 # Make the column header readable 32 post_count.short_description = 'Number of Posts'
List View Options Explained
| Option | What It Does |
|---|---|
list_display | Tuple of field names or callable methods to show as columns |
list_filter | Adds a filter sidebar. Works with ForeignKey, BooleanField, DateField, and choices |
search_fields | Adds a search box. Use __ for related fields (e.g., author__username) |
list_editable | Allows inline editing of specific fields directly in the list |
list_per_page | Number of records per page (default: 100) |
ordering | Default sort order |
prepopulated_fields | Auto-fills one field based on another (great for slugs) |
🏛️ 4. The Complete Post Admin
Now let's build a rich admin interface for the Post model.
1# blog/admin.py 2 3from django.contrib import admin 4from django.db.models import Count 5from .models import Category, Post, Comment 6 7 8# ------------------- INLINE MODELS ------------------- 9 10class CommentInline(admin.TabularInline): 11 """ 12 Allows editing comments directly on the Post admin page. 13 TabularInline shows them in a compact table format. 14 """ 15 model = Comment 16 extra = 0 # Don't show empty extra rows by default 17 readonly_fields = ['created_at'] # Prevent editing the timestamp 18 19 20# ------------------- CATEGORY ADMIN ------------------- 21 22@admin.register(Category) 23class CategoryAdmin(admin.ModelAdmin): 24 list_display = ['name', 'post_count', 'is_active'] 25 list_filter = ['is_active'] 26 search_fields = ['name', 'description'] 27 prepopulated_fields = {'slug': ('name',)} 28 29 def post_count(self, obj): 30 return obj.posts.count() 31 post_count.short_description = 'Posts' 32 33 34# ------------------- POST ADMIN ------------------- 35 36@admin.register(Post) 37class PostAdmin(admin.ModelAdmin): 38 """ 39 Full-featured admin for managing blog posts. 40 """ 41 42 # 1. LIST VIEW CONFIGURATION 43 list_display = [ 44 'title', 'author', 'category', 'status', 45 'view_count', 'is_featured', 'created_at' 46 ] 47 list_filter = [ 48 'status', 'category', 'created_at', 'is_featured' 49 ] 50 search_fields = [ 51 'title', 'content', 'author__username', 'author__email' 52 ] 53 prepopulated_fields = {'slug': ('title',)} 54 date_hierarchy = 'published_at' # Adds a date-based drill-down navigation 55 inlines = [CommentInline] # Show comments inline 56 57 # 2. CUSTOM BULK ACTIONS 58 actions = ['make_published', 'make_draft'] 59 60 # 3. FORM LAYOUT WITH FIELDSETS 61 fieldsets = ( 62 ('Content', { 63 'fields': ('title', 'slug', 'author', 'content', 'excerpt') 64 }), 65 ('Categorization', { 66 'fields': ('category', 'tags') 67 }), 68 ('Media', { 69 'fields': ('featured_image',) 70 }), 71 ('Settings', { 72 'fields': ('status', 'is_featured', 'published_at') 73 }), 74 ('Statistics', { 75 'fields': ('view_count',), 76 'classes': ('collapse',) # Collapsible section 77 }), 78 ) 79 80 # 4. CUSTOM ACTION: Publish selected posts 81 @admin.action(description='Mark selected posts as published') 82 def make_published(self, request, queryset): 83 """ 84 Bulk action to change status of selected posts to 'published'. 85 """ 86 updated = queryset.update(status='published') 87 self.message_user( 88 request, 89 f'{updated} post(s) were successfully marked as published.' 90 ) 91 92 # 5. CUSTOM ACTION: Draft selected posts 93 @admin.action(description='Mark selected posts as draft') 94 def make_draft(self, request, queryset): 95 updated = queryset.update(status='draft') 96 self.message_user( 97 request, 98 f'{updated} post(s) were successfully marked as draft.' 99 ) 100 101 # 6. QUERY OPTIMIZATION 102 def get_queryset(self, request): 103 """ 104 Override to use select_related, reducing database queries. 105 Without this, each post row triggers extra queries for author and category. 106 """ 107 return super().get_queryset(request).select_related('author', 'category')
Admin Interface Breakdown
┌─────────────────────────────────────────────────────────────┐
│ Django Admin > Posts │
├─────────────────────────────────────────────────────────────┤
│ [Add Post] [Action: Make Published ▼] [Go] │
│ │
│ ▼ Title ▼ Author ▼ Category ▼ Status Created │
│ ───────────────────────────────────────────────────────── │
│ Django Tips John Doe Tech Published Aug 14 │
│ Python Guide Jane Smith Coding Draft Aug 13 │
│ │
│ [Filter] │
│ By Status │
│ ☑ Published (5) │
│ ☐ Draft (2) │
│ ☐ Archived (1) │
└─────────────────────────────────────────────────────────────┘
🧩 5. Inline Models Explained
When a Post has many Comments, you don't want to jump between admin pages. Inlines let you edit related objects on the same page.
TabularInline vs. StackedInline
| Type | Appearance | Best For |
|---|---|---|
TabularInline | Compact table rows | Few fields (2-4), many records |
StackedInline | Full form blocks per item | Many fields, few records |
1# TabularInline (compact) 2class CommentInline(admin.TabularInline): 3 model = Comment 4 extra = 0 5 readonly_fields = ['created_at'] 6 7# StackedInline (full forms) 8class CommentInline(admin.StackedInline): 9 model = Comment 10 extra = 1 # Show one empty form for quick adding 11 fields = ['author', 'content', 'is_approved']
🎛️ 6. Fieldsets: Organizing the Edit Form
Without fieldsets, Django shows all fields in one long list. Fieldsets group them into logical sections.
1fieldsets = ( 2 ('Content', { 3 'fields': ('title', 'slug', 'author', 'content', 'excerpt') 4 }), 5 ('Categorization', { 6 'fields': ('category', 'tags'), 7 'description': 'Organize your post with categories and tags.' 8 }), 9 ('Media', { 10 'fields': ('featured_image',) 11 }), 12 ('Settings', { 13 'fields': ('status', 'is_featured', 'published_at'), 14 'classes': ('wide',) # Wider input fields 15 }), 16 ('Statistics', { 17 'fields': ('view_count',), 18 'classes': ('collapse',) # Hidden by default, click to expand 19 }), 20)
Available CSS classes:
'collapse'— Collapsible section (hidden by default)'wide'— Extra horizontal space for fields'extrapretty'— Enhanced styling (deprecated in newer Django versions)
⚡ 7. Query Optimization in Admin
By default, if your list_display shows author.username and category.name, Django runs extra queries for every row. This is the N+1 problem.
The fix: Override get_queryset and use select_related.
1def get_queryset(self, request): 2 return super().get_queryset(request).select_related('author', 'category')
What select_related does:
It performs a SQL JOIN, fetching the related author and category data in the same query instead of separate queries.
For ManyToMany or reverse FK: Use prefetch_related:
1def get_queryset(self, request): 2 return super().get_queryset(request).prefetch_related('tags', 'comments')
🧪 8. Practice Task — Export Posts as CSV
Task Requirements
Create a custom admin action that exports selected posts to a CSV file when downloaded.
Solution
1# blog/admin.py 2 3import csv 4from django.http import HttpResponse 5from django.contrib import admin 6from .models import Post 7 8 9@admin.register(Post) 10class PostAdmin(admin.ModelAdmin): 11 # ... existing configuration ... 12 13 actions = ['make_published', 'make_draft', 'export_as_csv'] 14 15 @admin.action(description='Export selected posts to CSV') 16 def export_as_csv(self, request, queryset): 17 """ 18 Export the selected posts as a CSV file download. 19 """ 20 # Define the CSV header 21 field_names = [ 22 'id', 'title', 'slug', 'author', 'category', 23 'status', 'view_count', 'created_at' 24 ] 25 26 # Create the HTTP response with CSV content type 27 response = HttpResponse(content_type='text/csv') 28 response['Content-Disposition'] = 'attachment; filename=posts.csv' 29 30 # Create CSV writer 31 writer = csv.writer(response) 32 33 # Write header row 34 writer.writerow(field_names) 35 36 # Write data rows 37 for post in queryset: 38 writer.writerow([ 39 post.id, 40 post.title, 41 post.slug, 42 post.author.username if post.author else '', 43 post.category.name if post.category else '', 44 post.status, 45 post.view_count, 46 post.created_at.strftime('%Y-%m-%d %H:%M') 47 ]) 48 49 return response
How It Works
- The
@admin.actiondecorator registers the method as a bulk action querysetcontains all selected recordsHttpResponsewithtext/csvtells the browser it's a file downloadContent-Disposition: attachmentforces download instead of displaying- Python's built-in
csvmodule handles proper formatting
To use it:
- Go to the Post admin list view
- Check the boxes next to posts you want to export
- Select "Export selected posts to CSV" from the Action dropdown
- Click Go — your browser downloads
posts.csv
❌ Common Mistakes & How to Fix Them
| Mistake | Error / Symptom | Solution |
|---|---|---|
| Model not registered | Model doesn't appear in admin | Use @admin.register(Model) or admin.site.register() |
list_display references missing field | AttributeError | Ensure the field or method exists on ModelAdmin or model |
search_fields on wrong type | Search doesn't work | search_fields only works with text-like fields |
prepopulated_fields without slug field | Nothing auto-fills | Ensure the target field is a SlugField |
Missing select_related | Admin loads very slowly | Override get_queryset and add select_related |
actions not a list | TypeError | actions must be a list: actions = ['action_name'] |
message_user typo | AttributeError | Use self.message_user(request, 'text') |
✅ Module 6 Summary
| Concept | Key Takeaway |
|---|---|
@admin.register | The modern way to register models with custom config |
ModelAdmin | The class that controls admin behavior for a model |
list_display | Columns shown in the list view |
list_filter | Sidebar filters for quick narrowing |
search_fields | Search box configuration |
prepopulated_fields | Auto-fill slugs from titles |
inlines | Edit related models on the same page |
TabularInline | Compact inline layout (table rows) |
StackedInline | Full inline layout (form blocks) |
fieldsets | Group fields into collapsible sections |
@admin.action | Create bulk operations on selected items |
get_queryset | Optimize database queries with select_related |
select_related | SQL JOIN for ForeignKey/OneToOne |
🚪 What's Next?
In Module 7, we will dive into Views.py — the logic layer of Django. You'll learn the difference between Function-Based Views (FBV) and Class-Based Views (CBV), how to handle HTTP requests and responses, work with Django's ORM in views, implement pagination, and use decorators like @login_required. We'll build the actual pages that users see when they visit your blog.
Before proceeding, make sure:
- All your models (
Post,Category,Comment) are registered in admin - You can see the custom
post_countcolumn in the Category admin - The Post admin shows comments inline
- You can use the "Make Published" bulk action
- The CSV export action works and downloads a proper file
- You understand why
select_relatedis needed inget_queryset
Your admin panel is now a powerful backoffice. Ready to build the views that power your frontend? 🖥️🚀