Creating Custom Actions¶
Actions are operations that can be performed on models. This guide covers creating custom actions for django-admin-deux.
Action Types¶
django-admin-deux has three action types:
| Type | Mixin | Scope | Example |
|---|---|---|---|
| General | GeneralActionMixin |
List-level (no selection) | Add, Export All, Import |
| Bulk | BulkActionMixin |
Multiple selected records | Delete Selected, Bulk Update |
| Record | RecordActionMixin |
Single record | Edit, Delete, Duplicate, View |
Basic Action Structure¶
All actions inherit from BaseAction and one action type mixin:
from djadmin.actions import BaseAction, GeneralActionMixin
class MyAction(GeneralActionMixin, BaseAction):
# Display configuration
label = 'My Action' # Required
icon = 'icon-name' # Optional
css_class = 'primary' # Optional
# Behavior
confirmation_required = False # Show confirmation?
http_method = 'GET' # 'GET' or 'POST'
Key attributes:
- label - Display name (required)
- icon - Icon identifier (optional)
- css_class - CSS class for styling (e.g., 'primary', 'danger')
- confirmation_required - Boolean to show confirmation dialog
- http_method - HTTP method for triggering action
General Actions (List-Level)¶
General actions operate on the list view without requiring record selection.
Simple Example: Export CSV¶
from djadmin.actions import BaseAction, GeneralActionMixin
from django.http import HttpResponse
import csv
class ExportCSVAction(GeneralActionMixin, BaseAction):
label = 'Export CSV'
icon = 'download'
def dispatch(self, request, **kwargs):
queryset = self.model.objects.all()
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{self.model._meta.model_name}.csv"'
writer = csv.writer(response)
fields = [f.name for f in self.model._meta.fields]
writer.writerow(fields)
for obj in queryset:
writer.writerow([getattr(obj, f) for f in fields])
return response
Register it:
# djadmin_hooks.py
@hookimpl
def djadmin_get_default_general_actions():
from .actions import ExportCSVAction
return [ExportCSVAction]
View-Based Action: Add¶
The AddAction uses ViewActionMixin to generate a full CreateView:
from djadmin.actions import BaseAction, GeneralActionMixin
from djadmin.actions.view_mixins import CreateViewActionMixin
class AddAction(GeneralActionMixin, CreateViewActionMixin, BaseAction):
label = 'Add'
icon = 'plus'
def get_template_name(self):
opts = self.model._meta
return [
f'djadmin/{opts.app_label}/{opts.model_name}_add.html',
'djadmin/actions/add.html',
]
def get_fields(self):
return self.model_admin.create_fields or '__all__'
See: djadmin/plugins/core/actions.py for full implementation.
Bulk Actions¶
Bulk actions operate on multiple selected records.
Example: Bulk Update Status¶
from djadmin.actions import BaseAction, BulkActionMixin
from django.shortcuts import redirect
from django.contrib import messages
class BulkPublishAction(BulkActionMixin, BaseAction):
label = 'Publish Selected'
icon = 'check'
confirmation_required = True
def post(self, request, *args, **kwargs):
queryset = self.get_queryset()
count = queryset.update(status='published')
messages.success(request, f'{count} items published.')
# Redirect back to list
opts = self.model._meta
return redirect(f'djadmin:{opts.app_label}_{opts.model_name}_list')
Example: Bulk Delete (Built-in)¶
class DeleteBulkAction(BulkActionMixin, BulkDeleteViewActionMixin, BaseAction):
label = 'Delete Selected'
icon = 'trash'
confirmation_required = True
def get_template_name(self):
opts = self.model._meta
return [
f'djadmin/{opts.app_label}/{opts.model_name}_delete_bulk.html',
'djadmin/actions/delete_bulk.html',
]
See: djadmin/plugins/core/actions.py for full implementation.
Record Actions¶
Record actions operate on a single model instance.
Example: Duplicate Record¶
from djadmin.actions import BaseAction, RecordActionMixin
from django.shortcuts import redirect
from django.contrib import messages
class DuplicateAction(RecordActionMixin, BaseAction):
label = 'Duplicate'
icon = 'copy'
def post(self, request, *args, **kwargs):
# Get the object
obj = self.get_object()
# Clone the object
obj.pk = None
obj.save()
messages.success(request, f'Duplicated: {obj}')
opts = self.model._meta
return redirect(f'djadmin:{opts.app_label}_{opts.model_name}_list')
Example: Custom URL Pattern¶
Override get_url_pattern() to customize the action's URL:
class ApproveAction(RecordActionMixin, BaseAction):
label = 'Approve'
def get_url_pattern(self):
"""Custom URL pattern"""
opts = self.model._meta
return f'{opts.app_label}/{opts.model_name}/<int:pk>/approve/'
def post(self, request, *args, **kwargs):
obj = self.get_object()
obj.approved = True
obj.save()
messages.success(request, f'Approved: {obj}')
return redirect(...)
View Action Mixins¶
For actions that need full Django views (forms, CRUD operations), use view action mixins:
| Mixin | Base View | Use Case |
|---|---|---|
CreateViewActionMixin |
CreateView | Create new record |
UpdateViewActionMixin |
UpdateView | Edit existing record |
DeleteViewActionMixin |
DeleteView | Delete with confirmation |
BulkDeleteViewActionMixin |
Custom | Delete multiple records |
FormViewActionMixin |
FormView | Custom forms |
TemplateViewActionMixin |
TemplateView | Display-only pages |
Example: Edit Action:
from djadmin.actions.view_mixins import UpdateViewActionMixin
class EditAction(RecordActionMixin, UpdateViewActionMixin, BaseAction):
label = 'Edit'
def get_template_name(self):
opts = self.model._meta
return [
f'djadmin/{opts.app_label}/{opts.model_name}_edit.html',
'djadmin/actions/edit.html',
]
def get_fields(self):
return self.model_admin.update_fields or '__all__'
def get_form_class(self):
if self.model_admin.update_form_class:
return self.model_admin.update_form_class
# Auto-generate form...
Permissions¶
Control action access with declarative permissions (Milestone 5):
from djadmin.plugins.permissions import IsSuperuser, HasDjangoPermission
class SensitiveAction(RecordActionMixin, BaseAction):
label = 'Sensitive Operation'
# Declarative permission: Only superusers can access
permission_class = IsSuperuser()
django_permission_name = 'change'
Permission Classes:
- AllowAny - No restrictions
- IsAuthenticated - Requires authenticated user
- IsStaff - Requires staff status
- IsSuperuser - Requires superuser status
- HasDjangoPermission() - Checks Django model permissions
Permission Composition:
# Require staff AND specific permission
permission_class = IsStaff() & HasDjangoPermission()
# Allow staff OR superuser
permission_class = IsStaff() | IsSuperuser()
# Authenticated but NOT superuser
permission_class = IsAuthenticated() & ~IsSuperuser()
Configurable Actions¶
Make actions configurable via ModelAdmin:
class ExportAction(GeneralActionMixin, BaseAction):
label = 'Export'
export_format = 'csv' # Default
def dispatch(self, request, *args, **kwargs):
# Use self.export_format
if self.export_format == 'csv':
return self._export_csv()
elif self.export_format == 'json':
return self._export_json()
Configure in ModelAdmin:
class BookAdmin(ModelAdmin):
def customize_actions(self):
"""Override to customize actions"""
actions = super().customize_actions()
# Find and configure export action
for action in actions['general']:
if isinstance(action, ExportAction):
action.export_format = 'json'
return actions
Testing Actions¶
# tests/test_actions.py
import pytest
from django.test import RequestFactory
from myapp.actions import ExportCSVAction
from examples.webshop.models import Product
@pytest.mark.django_db
def test_export_csv_action(admin_site, product_factory):
# Create test data
products = product_factory.create_batch(5)
# Initialize action
from djadmin import ModelAdmin
model_admin = ModelAdmin(Product, admin_site)
action = ExportCSVAction(Product, model_admin, admin_site)
# Execute action via view
factory = RequestFactory()
request = factory.get('/')
view = action.get_view_class().as_view()
response = view(request)
# Verify response
assert response['Content-Type'] == 'text/csv'
assert 'product.csv' in response['Content-Disposition']
Advanced Patterns¶
Action with Form (Modal)¶
For actions that show a form dialog:
from djadmin.actions.base import FormActionMixin
from django import forms
class SendEmailForm(forms.Form):
subject = forms.CharField()
message = forms.TextField()
class SendEmailAction(BulkActionMixin, FormActionMixin, BaseAction):
label = 'Send Email'
form_class = SendEmailForm
def form_valid(self, request, form, queryset=None, **kwargs):
# Send emails to selected records
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
for obj in queryset:
send_mail(subject, message, 'from@example.com', [obj.email])
messages.success(request, f'Sent emails to {queryset.count()} recipients')
return redirect(...)
Conditional Actions¶
For state-dependent actions, check state in the execute() method:
class PublishAction(RecordActionMixin, BaseAction):
label = 'Publish'
permission_class = IsStaff() & HasDjangoPermission(perm='change')
def post(self, request, *args, **kwargs):
obj = self.get_object()
# Check state at execution time
if obj.status == 'published':
messages.error(request, 'Already published')
return redirect(...)
obj.status = 'published'
obj.save()
messages.success(request, f'Published: {obj}')
return redirect(...)
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.
Next Steps¶
- Hook Reference - How to register actions via hooks
- Examples - Real-world action patterns
- Built-in actions:
djadmin/plugins/core/actions.py - Action base classes:
djadmin/actions/base.py - View mixins:
djadmin/actions/view_mixins.py