Skip to main content

Implementing Admin Actions

To create and register bulk actions that can be performed on multiple objects in the Django admin, you define an action function and register it using the @admin.action decorator or the actions attribute of a ModelAdmin.

from django.contrib import admin
from django.http import StreamingHttpResponse
from io import StringIO
from wsgiref.util import FileWrapper
from django.contrib.admin.options import ActionLocation

@admin.action(
description="Download subscription",
description_plural="Download selected subscriptions",
location=(ActionLocation.CHANGE_LIST, ActionLocation.CHANGE_FORM),
)
def download(modeladmin, request, queryset):
"""
An action that generates a file download for selected objects.
Works on both the change list and individual change forms.
"""
if queryset.count() > 1:
buf = StringIO("This is the content of the file for multiple items")
else:
obj = queryset.get()
buf = StringIO(f"This is the content of the file written by {obj.name}")

return StreamingHttpResponse(FileWrapper(buf), content_type="text/plain")

class SubscriberAdmin(admin.ModelAdmin):
list_display = ["name", "email"]
actions = [download]

Specifying Action Locations

By default, actions are only displayed on the "Change List" (the table view of multiple objects). You can use the ActionLocation enum from django.contrib.admin.options to make actions available on the "Change Form" (the individual object edit page) as well.

  • ActionLocation.CHANGE_LIST: Displays the action in the dropdown menu above the object list.
  • ActionLocation.CHANGE_FORM: Displays the action as a button or option on the individual object's edit page.

When an action is triggered from a CHANGE_FORM, the queryset passed to the action function will contain only the single object currently being viewed.

Customizing the Action Form

You can extend the interface of the action selection area by subclassing ActionForm from django.contrib.admin.helpers. This allows you to add extra fields (like confirmation checkboxes) or custom media.

from django import forms
from django.contrib.admin.helpers import ActionForm

class MediaActionForm(ActionForm):
# Add a custom field to the action selection area
confirm_action = forms.BooleanField(required=False, label="I confirm this action")

class Media:
js = ["admin/js/custom_action_logic.js"]

class SubscriberAdmin(admin.ModelAdmin):
actions = ["mail_admin"]
action_form = MediaActionForm

The base ActionForm provides two standard fields:

  • action: The ChoiceField containing the list of available actions.
  • select_across: A BooleanField (rendered as a hidden input) used to track if the user has selected all objects across all pages.

Accessing Action Metadata

The Action class in django.contrib.admin.options is used to store metadata about registered actions. While older versions of Django used tuples to represent actions, this codebase uses the Action dataclass.

If you are programmatically inspecting actions, access attributes directly rather than using index-based access or unpacking:

# Recommended approach using Action attributes
def process_action(action_obj):
func = action_obj.func
name = action_obj.name
description = action_obj.description
locations = action_obj.locations

Troubleshooting and Gotchas

  • Select Across Limitation: When an action is executed from the CHANGE_FORM (individual object view), the select_across functionality is automatically disabled (forced to False).
  • Deprecation Warning (Tuple Unpacking): Unpacking an Action object as a tuple (e.g., func, name, desc = action) is deprecated and will trigger a RemovedInDjango70Warning. Always use attribute access.
  • Custom get_actions Signature: If you override get_actions() or get_action_choices() in your ModelAdmin, you must include the action_location parameter in the method signature to avoid deprecation warnings.
  • Response Handling: If your action function returns an HttpResponse (or a subclass like StreamingHttpResponse), the admin will return that response to the user. If it returns None, the admin will redirect the user back to the original view.