Actions Guide¶
This guide provides a comprehensive overview of the action system in django-admin-deux.
What Are Actions?¶
Actions are operations that can be performed on models. They replace traditional view URLs with a flexible, composable system where every operation is an action.
Key Concepts: - Actions are objects that encapsulate behavior, display, and permissions - Actions can be general (main entry points), list (no selection), bulk (multiple records), or record (single record) - Actions are pluggable - plugins can provide default actions - Actions are customizable - override or replace any action
The Four Action Types¶
django-admin-deux organizes actions into four types based on their scope and purpose:
| Type | Scope | Selection Required | Example Use Cases |
|---|---|---|---|
| General Actions | Entry points | No | ListView, Dashboard links, Add new record, Import, Export all |
| Bulk Actions | Multiple records | Yes (checkboxes) | Delete selected, Bulk update, Export selected |
| Record Actions | Single record | No (per-row) | Edit, Delete, View, Duplicate |
Visual Layout¶
┌─────────────────────────────────────────┐
│ Product List │
│ │
│ [Add Product] [Import] │ ← General Actions
│ │
│ [☐] Select All │
│ ┌─────────────────────────────────────┐│
│ │☐ Product A [Edit] [Delete] ││ ← Record Actions
│ │☐ Product B [Edit] [Delete] ││
│ │☐ Product C [Edit] [Delete] ││
│ └─────────────────────────────────────┘│
│ │
│ [Delete Selected] [Update Status] │ ← Bulk Actions (shown when items selected)
└─────────────────────────────────────────┘
General Actions¶
General actions operate without requiring a specific record. They work at the model/queryset level and include:
- Entry points - Links in dashboards that navigate to your ModelAdmin (e.g.,
ListAction) - Toolbar actions - Buttons in the ListView toolbar for operations like adding records or importing data (e.g.,
AddAction,ImportAction)
The key distinction: general actions don't need a specific record to operate on, unlike record actions which always operate on a single object.
Default General Actions¶
The core plugin provides ListAction and AddAction by default:
from djadmin import ModelAdmin, register
@register(Product)
class ProductAdmin(ModelAdmin):
# ListAction and AddAction are automatically included
# ListAction: Creates dashboard link to the ListView
# AddAction: Creates "Add" button in toolbar that redirects to CreateView
pass
Removing General Actions¶
You can remove toolbar actions to create a read-only admin (note: ListAction is always needed to generate the list view):
from djadmin.plugins.core.actions import ListAction
@register(Product)
class ReadOnlyProductAdmin(ModelAdmin):
general_actions = [ListAction] # Only list view, no add button
record_actions = [] # No edit/delete
bulk_actions = [] # No bulk operations
Important: Always include ListAction in your general_actions unless you're completely replacing the list view functionality. Without it, the ModelAdmin won't generate a list view.
Custom General Actions¶
Add your own toolbar actions (remember to include ListAction):
from djadmin.actions import BaseAction, GeneralActionMixin
from djadmin.plugins.core.actions import ListAction, AddAction
class ImportAction(GeneralActionMixin, BaseAction):
label = 'Import'
icon = 'upload'
css_class = 'secondary'
def get_template_name(self):
return 'myapp/import.html'
def execute(self, request, **kwargs):
# Handle import logic
if request.method == 'POST':
file = request.FILES['file']
# Process file...
messages.success(request, "Import successful")
return redirect(...)
# Show import form
return render(request, self.get_template_name(), {})
@register(Product)
class ProductAdmin(ModelAdmin):
general_actions = [ListAction, AddAction, ImportAction]
General Action Examples¶
# Export all records
class ExportAllAction(GeneralActionMixin, BaseAction):
label = 'Export All'
icon = 'download'
def execute(self, request, **kwargs):
queryset = self.model.objects.all()
# Generate CSV/Excel
return generate_csv_response(queryset)
# Batch import
class BatchImportAction(GeneralActionMixin, BaseAction):
label = 'Batch Import'
icon = 'upload'
# Analytics dashboard
class AnalyticsAction(GeneralActionMixin, BaseAction):
label = 'Analytics'
icon = 'chart'
Bulk Actions¶
Bulk actions operate on multiple selected records. Users select checkboxes in the ListView, then choose a bulk action.
Default Bulk Actions¶
The core plugin provides DeleteBulkAction by default:
@register(Product)
class ProductAdmin(ModelAdmin):
# DeleteBulkAction is automatically included
# Shows confirmation before deleting selected records
pass
Custom Bulk Actions¶
from djadmin.actions import BaseAction, BulkActionMixin
class ActivateBulkAction(BulkActionMixin, BaseAction):
label = 'Activate Selected'
icon = 'check-circle'
confirmation_required = True
def execute(self, request, queryset, **kwargs):
count = queryset.update(status='active')
messages.success(request, f"Activated {count} products")
return redirect(self.get_success_url())
def get_success_url(self):
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_list',
current_app=self.admin_site.name,
)
@register(Product)
class ProductAdmin(ModelAdmin):
bulk_actions = [DeleteBulkAction, ActivateBulkAction]
Bulk Action with Form¶
class BulkUpdatePriceAction(BulkActionMixin, BaseAction):
label = 'Update Prices'
icon = 'dollar'
def get_template_name(self):
return 'myapp/bulk_update_price.html'
def execute(self, request, queryset, **kwargs):
if request.method == 'POST':
percentage = float(request.POST['percentage'])
for product in queryset:
product.price *= (1 + percentage / 100)
product.save()
count = queryset.count()
messages.success(
request,
f"Updated prices for {count} products"
)
return redirect(self.get_success_url())
# Show form
context = {
'queryset': queryset,
'count': queryset.count(),
}
return render(request, self.get_template_name(), context)
@register(Product)
class ProductAdmin(ModelAdmin):
bulk_actions = [DeleteBulkAction, BulkUpdatePriceAction]
Bulk Action Examples¶
# Bulk status change
class BulkStatusAction(BulkActionMixin, BaseAction):
label = 'Change Status'
icon = 'toggle'
# Bulk category assignment
class BulkCategoryAction(BulkActionMixin, BaseAction):
label = 'Assign Category'
icon = 'folder'
# Bulk export
class ExportSelectedAction(BulkActionMixin, BaseAction):
label = 'Export Selected'
icon = 'download'
Record Actions¶
Record actions operate on a single record. They appear as buttons in each ListView row.
Default Record Actions¶
The core plugin provides ViewAction, EditAction, and DeleteAction by default:
@register(Product)
class ProductAdmin(ModelAdmin):
# ViewAction, EditAction, and DeleteAction are automatically included
# - ViewAction: Read-only detail view (for users with 'view' but not 'change' permission)
# - EditAction: Edit form (UpdateView)
# - DeleteAction: Delete confirmation
pass
Note: Actions are automatically filtered based on user permissions. Users only see actions they have permission to execute:
- Users with view-only permission see ViewAction
- Users with change permission see EditAction and DeleteAction
- Superusers see all actions
Custom Record Actions¶
from djadmin.actions import BaseAction, RecordActionMixin
class DuplicateAction(RecordActionMixin, BaseAction):
label = 'Duplicate'
icon = 'copy'
def execute(self, request, obj, **kwargs):
# Create a copy
obj.pk = None
obj.name = f"{obj.name} (Copy)"
obj.sku = f"{obj.sku}-copy"
obj.save()
messages.success(request, f"Duplicated '{obj.name}'")
return redirect(self.get_success_url())
def get_success_url(self):
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_list',
current_app=self.admin_site.name,
)
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [
EditAction,
DuplicateAction,
DeleteAction,
]
Conditional Record Actions¶
For state-dependent actions, check state in the execute() method:
class PublishAction(RecordActionMixin, BaseAction):
label = 'Publish'
icon = 'check'
css_class = 'success'
permission_class = IsStaff() & HasDjangoPermission(perm='change')
def execute(self, request, obj, **kwargs):
# Check state at execution time
if obj.status == 'published':
messages.error(request, f"'{obj.title}' is already published")
return redirect(self.get_success_url())
obj.status = 'published'
obj.published_date = timezone.now()
obj.save()
messages.success(request, f"Published '{obj.title}'")
return redirect(self.get_success_url())
class UnpublishAction(RecordActionMixin, BaseAction):
label = 'Unpublish'
icon = 'x'
css_class = 'warning'
permission_class = IsStaff() & HasDjangoPermission(perm='change')
def execute(self, request, obj, **kwargs):
# Check state at execution time
if obj.status == 'draft':
messages.error(request, f"'{obj.title}' is already a draft")
return redirect(self.get_success_url())
obj.status = 'draft'
obj.published_date = None
obj.save()
messages.success(request, f"Unpublished '{obj.title}'")
return redirect(self.get_success_url())
@register(Post)
class PostAdmin(ModelAdmin):
record_actions = [
EditAction,
PublishAction,
UnpublishAction,
DeleteAction,
]
Note: For per-record visibility (e.g., showing "Publish" only for drafts), handle this in templates rather than at the action level, as action filtering happens at the list level, not per-record.
Record Action Examples¶
# View details (read-only)
class ViewAction(RecordActionMixin, BaseAction):
label = 'View'
icon = 'eye'
# Send email
class SendEmailAction(RecordActionMixin, BaseAction):
label = 'Email Customer'
icon = 'mail'
# Generate PDF
class GeneratePDFAction(RecordActionMixin, BaseAction):
label = 'Download PDF'
icon = 'file'
# Clone with relations
class CloneAction(RecordActionMixin, BaseAction):
label = 'Clone'
icon = 'copy'
Action Properties¶
All actions support these properties:
Display Properties¶
class MyAction(BaseAction):
label = 'My Action' # Required: Display text
icon = 'icon-name' # Optional: Icon identifier
css_class = 'primary' # Optional: CSS classes (primary, secondary, success, danger, warning)
Behavior Properties¶
class MyAction(BaseAction):
confirmation_required = True # Show confirmation before execute
http_method = 'POST' # HTTP method (GET or POST)
URL Configuration¶
class MyAction(BaseAction):
def get_url_pattern(self) -> str:
"""Override to customize URL pattern"""
return f'{self.model._meta.app_label}/{self.model._meta.model_name}/custom/'
@property
def url_name(self) -> str:
"""Get the URL name for reverse()"""
return f'{self.model._meta.app_label}_{self.model._meta.model_name}_myaction'
Permission Control (New in Milestone 5)¶
Actions support declarative permission control through the permission_class attribute:
from djadmin.plugins.permissions import IsStaff, IsSuperuser
class MyAction(BaseAction):
# Declarative permission using the new permission system
permission_class = IsStaff() # Only staff users can see/execute this action
# Django permission name for this action (used by HasDjangoPermission)
django_permission_name = 'view' # 'add', 'change', 'delete', or 'view'
Available permission classes:
- AllowAny - No restrictions
- IsAuthenticated - Requires authenticated user
- IsStaff - Requires staff status
- IsSuperuser - Requires superuser status
- HasDjangoPermission() - Checks Django model permissions (auto-detects from django_permission_name)
Permission composition:
from djadmin.plugins.permissions import IsStaff, HasDjangoPermission
# AND: Both must pass
permission_class = IsStaff() & HasDjangoPermission()
# OR: Either can pass
permission_class = IsStaff() | IsSuperuser()
# NOT: Invert result
permission_class = ~HasDjangoPermission(perm='change')
Per-action permission overrides: Actions can override the ModelAdmin's default permission:
from djadmin.plugins.core.actions import ListAction, AddAction
@register(Product)
class ProductAdmin(ModelAdmin):
# Default permission for all actions
permission_class = IsStaff() & HasDjangoPermission()
# Override for specific action
general_actions = [
ListAction, # Uses default permission
AddAction(permission_class=IsSuperuser()), # Only superusers can add
]
Automatic action filtering (New in Phase 2.7): Actions are automatically filtered based on user permissions. Users only see actions they have permission to execute:
# If user has 'view' but NOT 'change' permission:
# - They see ViewAction (read-only)
# - They do NOT see EditAction or DeleteAction
For more information, see the Permissions System documentation (coming in Phase 4).
When to Use Each Action Type¶
Use General Actions When:¶
- Creating main entry points to your ModelAdmin (e.g., ListView link, Dashboard link)
- Providing navigation from dashboards
- Operating on the entire model/queryset without selection
- Creating new records
- No specific records need to be selected
- Operation doesn't depend on individual records
Examples: ListView link, Dashboard link, Add new, Import, Export all, Clear cache, Generate report
Use Bulk Actions When:¶
- Operating on multiple selected records
- User needs to choose which records to process
- Operation applies the same logic to each selected record
- Performing batch updates or operations
Examples: Delete selected, Bulk update status, Bulk category assignment, Export selected
Use Record Actions When:¶
- Operating on a single specific record
- Action is contextual to one record
- Each record gets a button in the list view
- Operation is commonly used
Examples: Edit, Delete, View details, Duplicate, Send email, Download PDF
Default Actions Provided by Core Plugin¶
The core plugin (djadmin.plugins.core) provides these default actions:
from djadmin.plugins.core.actions import (
ListAction, # General action: Creates list view and dashboard link
AddAction, # General action: Create new record
ViewAction, # Record action: View existing record (read-only)
EditAction, # Record action: Edit existing record
DeleteAction, # Record action: Delete single record
DeleteBulkAction, # Bulk action: Delete multiple records
)
These are automatically applied unless you override the action lists in your ModelAdmin.
New in Phase 2.7: ViewAction provides read-only access for users with 'view' but not 'change' permission. It automatically appears/disappears based on user permissions.
Customizing Default Actions¶
Override Action Lists¶
Replace defaults entirely:
from djadmin.plugins.core.actions import ListAction, AddAction, EditAction
@register(Product)
class ProductAdmin(ModelAdmin):
general_actions = [ListAction, AddAction] # List view and Add button
record_actions = [EditAction] # Only Edit, no Delete
bulk_actions = [] # No bulk actions
Extend Default Actions¶
Add to defaults:
from djadmin.plugins.core.actions import (
ListAction, AddAction, EditAction, DeleteAction, DeleteBulkAction
)
@register(Product)
class ProductAdmin(ModelAdmin):
general_actions = [ListAction, AddAction, ImportAction, ExportAction]
record_actions = [EditAction, DuplicateAction, DeleteAction]
bulk_actions = [DeleteBulkAction, ActivateBulkAction]
Customize Existing Actions¶
Subclass and override:
from djadmin.plugins.core.actions import ListAction, AddAction
class CustomAddAction(AddAction):
label = 'Create Product'
icon = 'plus-circle'
css_class = 'success'
def get_fields(self):
# Custom field selection
return ['name', 'sku', 'price']
@register(Product)
class ProductAdmin(ModelAdmin):
general_actions = [ListAction, CustomAddAction]
Creating Custom Actions¶
For detailed information on creating custom actions, see the Plugin Development Guide.
Quick Example¶
from djadmin.actions import BaseAction, RecordActionMixin
from django.shortcuts import redirect
from django.contrib import messages
class MyCustomAction(RecordActionMixin, BaseAction):
label = 'My Action'
icon = 'star'
css_class = 'primary'
def execute(self, request, obj, **kwargs):
# Your action logic here
obj.is_featured = True
obj.save()
messages.success(request, f"Featured '{obj.name}'")
return redirect(self.get_success_url())
def get_success_url(self):
opts = self.model._meta
return reverse(
f'djadmin:{opts.app_label}_{opts.model_name}_list',
current_app=self.admin_site.name,
)
@register(Product)
class ProductAdmin(ModelAdmin):
record_actions = [EditAction, MyCustomAction, DeleteAction]
Action Display in Templates¶
In Dashboards¶
General actions appear in dashboard tables:
{% for action in model_admin.general_actions %}
<a href="{{ action.url }}">{{ action.label }}</a>
{% endfor %}
In ListView Toolbar¶
General actions appear in the toolbar:
{% for action in general_actions %}
<a href="{{ action.url }}" class="{{ action.css_class }}">
{{ action.icon }} {{ action.label }}
</a>
{% endfor %}
In ListView Rows¶
Record actions appear for each record:
{% for action in record_actions %}
<a href="{{ action.url }}" class="{{ action.css_class }}">
{{ action.icon }} {{ action.label }}
</a>
{% endfor %}
Bulk Actions¶
Bulk actions appear when records are selected:
<form method="post">
{% for action in bulk_actions %}
<button formaction="{{ action.url }}" class="{{ action.css_class }}">
{{ action.icon }} {{ action.label }}
</button>
{% endfor %}
</form>
Best Practices¶
- Use descriptive labels: Make action purpose clear
- Choose appropriate icons: Visual cues help users
- Apply CSS classes: Use semantic classes (primary, danger, etc.)
- Require confirmation for destructive actions: Always confirm before delete
- Provide feedback: Use messages to confirm success
- Check permissions: Implement
has_permission()for sensitive actions - Use conditional display: Implement
is_available()for contextual actions - Follow Django patterns: Use Django's CBV patterns and conventions
See Also¶
- CRUD Operations - How CRUD works via actions
- Basic Usage - ModelAdmin configuration overview
- Customization - Advanced customization techniques
- Plugin Development - Creating Actions - Detailed action creation guide