Introduction
Admin actions let you perform bulk operations on selected objects directly from the change list. In this lesson, you will learn how to create custom actions for publishing, exporting, and managing records in bulk.
Key Concepts
Admin action: A function that receives the ModelAdmin, the HTTP request, and a queryset of selected objects.
@admin.action: Decorator that sets the human-readable description shown in the action dropdown.
message_user(): Method to display success or error feedback after an action completes.
get_actions(): Method to dynamically add or remove actions based on user permissions.
Real World Context
In an e-commerce back office, a customer support team receives dozens of refund requests daily. Without bulk actions, each order must be opened individually to change its status. A custom 'Mark as refunded' action lets the team select 20 orders at once from the change list and process them in a single click, saving hours of repetitive work.
Deep Dive
Admin actions let users perform bulk operations on selected objects.
Basic Action
pythonfrom django.contrib import admin from django.contrib import messages @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ['title', 'status', 'pub_date'] actions = ['make_published', 'make_draft', 'export_as_csv'] @admin.action(description='Mark selected articles as published') def make_published(self, request, queryset): count = queryset.update(status='published') self.message_user( request, f'{count} articles marked as published.', messages.SUCCESS ) @admin.action(description='Mark selected articles as draft') def make_draft(self, request, queryset): count = queryset.update(status='draft') self.message_user(request, f'{count} articles marked as draft.')
Export Action
pythonimport csv from django.http import HttpResponse @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): actions = ['export_as_csv'] @admin.action(description='Export selected as CSV') def export_as_csv(self, request, queryset): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="articles.csv"' writer = csv.writer(response) writer.writerow(['Title', 'Author', 'Status', 'Published']) # Header for article in queryset: writer.writerow([ article.title, article.author.username, article.status, article.pub_date, ]) return response
Action with Confirmation
pythonfrom django.contrib.admin import helpers from django.template.response import TemplateResponse @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): actions = ['delete_with_confirmation'] @admin.action(description='Delete with custom confirmation') def delete_with_confirmation(self, request, queryset): if 'apply' in request.POST: # Perform the action count = queryset.count() queryset.delete() self.message_user(request, f'Deleted {count} articles.') return None # Show confirmation page context = { 'title': 'Confirm Deletion', 'queryset': queryset, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 'opts': self.model._meta, } return TemplateResponse( request, 'admin/confirm_delete.html', context )
Conditional Actions
python@admin.register(Article) class ArticleAdmin(admin.ModelAdmin): def get_actions(self, request): actions = super().get_actions(request) # Remove delete action for non-superusers if not request.user.is_superuser: if 'delete_selected' in actions: del actions['delete_selected'] return actions @admin.action(description='Send notification to authors') def notify_authors(self, request, queryset): if not request.user.has_perm('blog.send_notifications'): self.message_user( request, 'You do not have permission to send notifications.', messages.ERROR ) return # Send notifications... for article in queryset: send_notification(article.author, f'Update on: {article.title}') self.message_user( request, f'Notifications sent to {queryset.count()} authors.' )
Global Actions
python# admin.py from django.contrib import admin def export_selected_objects(modeladmin, request, queryset): """Generic export action for any model.""" import csv from django.http import HttpResponse meta = modeladmin.model._meta field_names = [field.name for field in meta.fields] response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="{meta}.csv"' writer = csv.writer(response) writer.writerow(field_names) for obj in queryset: writer.writerow([getattr(obj, field) for field in field_names]) return response export_selected_objects.short_description = 'Export selected objects' # Add to all admin sites admin.site.add_action(export_selected_objects, 'export_selected')
Common Pitfalls
-
Forgetting to call message_user(): Actions that complete silently leave admins wondering if anything happened. Always provide feedback, even on partial success.
-
Not handling empty querysets: If the user selects no objects or the queryset returns zero matches, your action should detect this and display a warning instead of silently doing nothing.
-
Using queryset.delete() without confirmation: Destructive actions should either use Django's built-in deletion confirmation or implement a custom confirmation step to prevent accidental data loss.
Best Practices
-
Use @admin.action(permissions=[...]) to restrict actions to users with specific permissions rather than checking permissions inside the function body.
-
Return an HttpResponse for exports: Actions that generate files (CSV, PDF) should return an HttpResponse with the appropriate content type and Content-Disposition header.
-
Keep actions focused: Each action should do one thing. Instead of a single action with multiple behaviors based on flags, create separate named actions.
Summary
- Admin actions process selected objects in bulk from the change list.
- Define actions as methods on ModelAdmin and list them in the actions attribute.
- The @admin.action decorator sets the display name and optional permissions.
- Always call message_user() to provide feedback after the action completes.
- Use get_actions() to conditionally show or hide actions based on user permissions.