Skip to content

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