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.

Note: 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.

Note: This is correct, real Django admin configuration — but this website's own project doesn't run a Django server, so you won't see the inline table actually render on this page. Register these classes against your own TaskFlow project to see it in action.
Next up: Actions and inlines both live inside a single request-response cycle. Middleware looks at everything that happens around that cycle — for every request, on every view, admin included.