Skip to content

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

  1. Use descriptive labels: Make action purpose clear
  2. Choose appropriate icons: Visual cues help users
  3. Apply CSS classes: Use semantic classes (primary, danger, etc.)
  4. Require confirmation for destructive actions: Always confirm before delete
  5. Provide feedback: Use messages to confirm success
  6. Check permissions: Implement has_permission() for sensitive actions
  7. Use conditional display: Implement is_available() for contextual actions
  8. Follow Django patterns: Use Django's CBV patterns and conventions

See Also