Admin Actions & Inlines
So far, every change made in the admin has touched one row at a time — open a task, edit it,
save it. That's fine for a single record, but it breaks down the moment you need to do the same
thing to fifty of them. Admin actions solve the "do this to many rows at once"
problem, and inlines solve a different but related one: editing a parent object
and its closely related children — a Project and its Tasks — without bouncing between two
separate admin sections. Both are built directly into ModelAdmin, so neither
requires any new concepts beyond what Customizing the
Admin already introduced.
Bulk Operations from the Change List
Open the change list for any registered model in the admin and you'll notice a checkbox next to each row, plus a dropdown near the top labeled "Action". Tick a handful of rows, pick an action from that dropdown, hit "Go", and Django runs that action once against the whole selected QuerySet — not once per row in a loop you write, but a single function call you control, handed the entire selection at once.
Every model registered in the admin gets one action automatically, with no extra code: "Delete selected <model>s". Select several tasks that no longer matter, choose it, confirm on the page that follows, and all of them are gone in one operation. It's a small thing, but it's worth noticing before writing a custom action — it shows you the actions dropdown is already part of the admin you've been using since The Django Admin: Basics, not a new UI concept.
Writing a Custom Action: mark_completed
A custom action is just a plain function that takes three arguments: the current
ModelAdmin instance, the current request, and a QuerySet
of exactly the rows the admin user selected. Here's one worth having on any task tracker —
marking a batch of tasks as completed in one click, instead of opening each one and ticking a
checkbox:
# taskflow/admin.py
from django.contrib import admin
from .models import Task, Project
class TaskAdmin(admin.ModelAdmin):
list_display = ('title', 'project', 'priority', 'completed', 'due_date')
actions = ['mark_completed']
def mark_completed(self, request, queryset):
updated = queryset.update(completed=True)
self.message_user(request, f"{updated} task(s) marked as completed.")
mark_completed.short_description = "Mark selected tasks as completed"
admin.site.register(Task, TaskAdmin)
Two details matter here. First, queryset.update(completed=True) issues a single
SQL UPDATE statement covering every selected task, which is both simpler and far
faster than looping over each task and calling .save() individually. Second,
mark_completed.short_description sets the human-readable label shown in the actions
dropdown — without it, the dropdown would show the less friendly raw function name,
"Mark completed". Registering it on TaskAdmin via
actions = ['mark_completed'] is what makes it appear in the dropdown at all; leaving
that list empty means only the built-in delete action shows up.
self.message_user(...) is what puts that confirmation
banner ("3 task(s) marked as completed.") at the top of the change list after the action runs —
worth including on any custom action so the admin user gets clear feedback that something actually
happened.Inlines: Editing Related Objects on One Page
Right now, adding tasks to a new project means two trips through the admin: create the Project, save it, then switch over to the Task section and create each task separately, remembering to pick the right project from a dropdown every time. An inline collapses that into one page — a small embedded table (or set of forms) for a related model, sitting directly inside its parent's own admin page.
The most common inline style is TabularInline, which renders each related task as
one compact row in a table — good when there are only a few fields per related object and you
want to see several of them at a glance:
# taskflow/admin.py
class TaskInline(admin.TabularInline):
model = Task
extra = 2
fields = ('title', 'priority', 'due_date', 'completed')
model = Task tells Django which related model this inline edits — Django works out
the relationship automatically from Task's project ForeignKey. extra = 2
adds two blank rows for new tasks every time the page loads, on top of whatever tasks already
exist, so there's always room to add a couple more without an extra click. fields
narrows the inline down to just the columns that matter for a quick add — description and tags
can still be edited from Task's own admin page if needed.
StackedInline: The Spaced-Out Alternative
StackedInline renders the exact same related objects, but as a stacked, form-style
block per object instead of a compact table row — each field gets its own labeled line, with more
breathing room. It reads better when a related object has many fields, or when a field (a longer
description, for instance) genuinely needs more horizontal space than a table cell comfortably
gives it. For TaskFlow's Task model, with only a handful of short fields, TabularInline
is the better fit — but swapping one for the other is literally a one-word change:
# taskflow/admin.py
class TaskInline(admin.StackedInline):
model = Task
extra = 1
As a rule of thumb: reach for TabularInline when you want to scan several related
rows at once, and StackedInline when each related object has enough fields that a
single table row would feel cramped.
Combining Both: A ProjectAdmin with Its Tasks Inline
Attaching an inline to a parent's ModelAdmin is one line — an inlines
list, just like actions earlier. Here's ProjectAdmin wired up with the
TaskInline from above, so creating a brand-new project and adding its first few
tasks happens on a single screen:
# taskflow/admin.py
class TaskInline(admin.TabularInline):
model = Task
extra = 2
fields = ('title', 'priority', 'due_date', 'completed')
class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'owner')
inlines = [TaskInline]
admin.site.register(Project, ProjectAdmin)
admin.site.register(Task, TaskAdmin)
Open the admin's Project page now and each project shows a "Tasks" section underneath its own fields, pre-populated with any existing tasks plus two empty rows ready for new ones. Save the page, and Django creates or updates the project itself along with every filled-in inline row, all in one submission — no separate visit to Task's own admin section required for the common case of setting up a project and its first batch of work.