Skip to content

Plugin Examples

Real-world plugin patterns and examples for common use cases.

Export Plugin

Add CSV/Excel export functionality to list views.

Structure

# djadmin_export/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['export']

@hookimpl
def djadmin_get_default_general_actions():
    from .actions import ExportCSVAction, ExportExcelAction
    return [ExportCSVAction, ExportExcelAction]

CSV Export Action

# djadmin_export/actions.py
import csv
from django.http import HttpResponse
from djadmin.actions import BaseAction, GeneralActionMixin

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')
        filename = self.model._meta.model_name
        response['Content-Disposition'] = f'attachment; filename="{filename}.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

Configuration: Use ModelAdmin.export_fields to customize fields:

class ProductAdmin(ModelAdmin):
    export_fields = ['name', 'sku', 'price', 'stock_quantity']

Full implementation: Could use django-import-export or custom logic with pandas.


Import Plugin

Bulk import records from CSV/Excel files.

Structure

# djadmin_import/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['import']

@hookimpl
def djadmin_get_default_general_actions():
    from .actions import ImportCSVAction
    return [ImportCSVAction]

Import Action (Form Pattern)

# djadmin_import/actions.py
from django import forms
from django.shortcuts import redirect
from django.contrib import messages
from djadmin.actions import BaseAction, GeneralActionMixin
from djadmin.actions.base import FormActionMixin

class ImportForm(forms.Form):
    file = forms.FileField(label='CSV File')

class ImportCSVAction(GeneralActionMixin, FormActionMixin, BaseAction):
    label = 'Import CSV'
    icon = 'upload'
    form_class = ImportForm

    def form_valid(self, request, form, **kwargs):
        import csv
        uploaded_file = form.cleaned_data['file']

        # Parse CSV
        decoded = uploaded_file.read().decode('utf-8')
        reader = csv.DictReader(decoded.splitlines())

        # Create records
        count = 0
        for row in reader:
            self.model.objects.create(**row)
            count += 1

        messages.success(request, f'Imported {count} records')

        opts = self.model._meta
        return redirect(f'djadmin:{opts.app_label}_{opts.model_name}_list')

Extension: Add validation, error handling, preview step, field mapping.


Audit Log Plugin

Log all create/update/delete operations.

Structure

# djadmin_audit/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['audit']

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add audit logging mixin to all actions"""
    from djadmin.actions.base import BaseAction
    from .mixins import AuditLogMixin

    return {
        BaseAction: [AuditLogMixin]
    }

Audit Mixin

# djadmin_audit/mixins.py
from django.utils import timezone

class AuditLogMixin:
    """Mixin to log action execution via dispatch"""

    def dispatch(self, request, *args, **kwargs):
        # Log before dispatch
        from .models import AuditLog
        AuditLog.objects.create(
            user=request.user,
            action_type=self.__class__.__name__,
            model_name=f"{self.model._meta.app_label}.{self.model._meta.model_name}",
            timestamp=timezone.now(),
            ip_address=request.META.get('REMOTE_ADDR'),
        )

        # Continue with normal dispatch
        return super().dispatch(request, *args, **kwargs)

Audit Model

# djadmin_audit/models.py
from django.db import models
from django.conf import settings

class AuditLog(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    action_type = models.CharField(max_length=100)
    model_name = models.CharField(max_length=100)
    object_repr = models.TextField(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    ip_address = models.GenericIPAddressField(null=True)

    class Meta:
        ordering = ['-timestamp']

Extension: Track field changes (django-reversion), add audit trail view, export audit logs.


Search Plugin

Add search functionality to list views.

Structure

# djadmin_search/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['search']

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add search mixin to ListView"""
    from djadmin.actions.view_mixins import ListActionMixin
    from .mixins import SearchMixin
    return {ListActionMixin: [SearchMixin]}

@hookimpl
def djadmin_modify_queryset(queryset, request, view):
    """Apply search filtering"""
    search_term = request.GET.get('q', '').strip()
    if not search_term:
        return queryset

    # Get search fields from model_admin
    search_fields = getattr(view.model_admin, 'search_fields', [])
    if not search_fields:
        return queryset

    # Build Q objects for OR search
    from django.db.models import Q
    queries = [Q(**{f'{field}__icontains': search_term}) for field in search_fields]
    return queryset.filter(Q(*queries, _connector=Q.OR))

@hookimpl
def djadmin_add_context_data(context, request, view):
    """Add search term to context"""
    return {'search_term': request.GET.get('q', '')}

@hookimpl
def djadmin_get_action_view_assets(action):
    """Add search UI assets"""
    from djadmin import JSAsset, CSSAsset
    from djadmin.actions.view_mixins import ListActionMixin
    return {
        ListActionMixin: {
            'css': [CSSAsset(href='djadmin_search/css/search.css')],
            'js': [JSAsset(src='djadmin_search/js/search.js', defer=True)],
        }
    }

Search Mixin

# djadmin_search/mixins.py
class SearchMixin:
    """Mixin to add search box to list view"""
    pass  # Context and queryset handled by hooks

Template addition (in theme or plugin templates):

{# Add to list view template #}
<form method="get" class="search-form">
    <input type="search" name="q" value="{{ search_term }}" placeholder="Search...">
    <button type="submit">Search</button>
</form>

Extension: Full-text search (PostgreSQL), Elasticsearch integration, search history.


Filter Plugin

Add filtering sidebar to list views.

Structure

# djadmin_filter/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['filter']

@hookimpl
def djadmin_modify_queryset(queryset, request, view):
    """Apply filters from query string"""
    model_admin = view.model_admin
    list_filter = getattr(model_admin, 'list_filter', [])

    for filter_field in list_filter:
        if filter_field in request.GET:
            queryset = queryset.filter(**{filter_field: request.GET[filter_field]})

    return queryset

@hookimpl
def djadmin_add_context_data(context, request, view):
    """Add filter options to context"""
    model_admin = view.model_admin
    list_filter = getattr(model_admin, 'list_filter', [])

    filter_data = {}
    for filter_field in list_filter:
        # Get distinct values for this field
        values = view.model.objects.values_list(filter_field, flat=True).distinct()
        filter_data[filter_field] = list(values)

    return {'filter_data': filter_data, 'active_filters': dict(request.GET.items())}

Template:

{# Sidebar with filters #}
<aside class="filters-sidebar">
    {% for field, values in filter_data.items %}
        <div class="filter-group">
            <h4>{{ field|title }}</h4>
            {% for value in values %}
                <label>
                    <input type="checkbox" name="{{ field }}" value="{{ value }}"
                           {% if value in active_filters.field %}checked{% endif %}>
                    {{ value }}
                </label>
            {% endfor %}
        </div>
    {% endfor %}
</aside>

Extension: Date range filters, custom filter classes, faceted search.


Permissions Plugin

Fine-grained permission control for actions.

Note: As of Milestone 5, django-admin-deux has a built-in declarative permissions system. Use permission_class on actions instead of custom hooks.

# myapp/actions.py
from djadmin.actions import BaseAction, RecordActionMixin
from djadmin.plugins.permissions import IsStaff, HasDjangoPermission

class SensitiveAction(RecordActionMixin, BaseAction):
    label = 'Sensitive Operation'

    # Declarative permissions - automatically enforced
    permission_class = IsStaff() & HasDjangoPermission(perm='change')
    django_permission_name = 'change'

Custom Permission Mixin (Advanced)

# djadmin_permissions/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['advanced_permissions']

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add custom permission checks to actions"""
    from djadmin.actions.base import BaseAction
    from .mixins import CustomPermissionMixin

    return {
        BaseAction: [CustomPermissionMixin]
    }
# djadmin_permissions/mixins.py
from django.core.exceptions import PermissionDenied

class CustomPermissionMixin:
    """Custom permission logic via dispatch override"""

    def dispatch(self, request, *args, **kwargs):
        # Custom permission checks
        if not self.check_custom_permission(request):
            raise PermissionDenied("Custom permission check failed")

        return super().dispatch(request, *args, **kwargs)

    def check_custom_permission(self, request):
        # Your custom logic here
        return True

Extension: Row-level permissions, permission groups, custom permission logic.


Notification Plugin

Send notifications on specific actions.

Structure

# djadmin_notify/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['notify']

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add notification mixin to specific actions"""
    from djadmin.actions.view_mixins import CreateViewActionMixin, UpdateViewActionMixin
    from .mixins import NotificationMixin

    return {
        CreateViewActionMixin: [NotificationMixin],
        UpdateViewActionMixin: [NotificationMixin],
    }

Notification Mixin

# djadmin_notify/mixins.py
class NotificationMixin:
    """Send notifications on form success"""

    def form_valid(self, form):
        # Get the response from parent
        response = super().form_valid(form)

        # Send notification after successful save
        from .notifications import send_notification
        send_notification(
            user=self.request.user,
            action=self.__class__.__name__,
            object=self.object,
        )

        return response

Notification Service

# djadmin_notify/notifications.py
def send_notification(user, action, object):
    """Send notification (email, Slack, etc.)"""
    from django.core.mail import send_mail

    send_mail(
        subject=f'{action} performed on {object}',
        message=f'{user.username} performed {action} on {object}',
        from_email='admin@example.com',
        recipient_list=['manager@example.com'],
    )

Extension: Webhooks, Slack/Discord integration, notification preferences.


Multi-Tenancy Plugin

Filter data by tenant (organization, workspace, etc.).

Structure

# djadmin_tenancy/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['tenancy']

@hookimpl
def djadmin_modify_queryset(queryset, request, view):
    """Filter queryset by current tenant"""
    # Get current tenant from request/session
    tenant = getattr(request, 'tenant', None)
    if tenant and hasattr(queryset.model, 'tenant'):
        return queryset.filter(tenant=tenant)
    return queryset

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add tenant mixin to create actions"""
    from djadmin.actions.view_mixins import CreateViewActionMixin
    from .mixins import TenantMixin

    return {
        CreateViewActionMixin: [TenantMixin]
    }

Tenant Mixin

# djadmin_tenancy/mixins.py
class TenantMixin:
    """Set tenant on new records"""

    def form_valid(self, form):
        # Set tenant before saving
        tenant = getattr(self.request, 'tenant', None)
        if tenant:
            form.instance.tenant = tenant

        return super().form_valid(form)

Extension: Tenant switching UI, cross-tenant queries (superusers), tenant-specific settings.


Versioning Plugin

Track record versions and enable rollback.

Structure

# djadmin_versions/djadmin_hooks.py
from djadmin.plugins import hookimpl

@hookimpl
def djadmin_provides_features():
    return ['versions']

@hookimpl
def djadmin_get_default_record_actions():
    """Add version history action"""
    from .actions import VersionHistoryAction
    return [VersionHistoryAction]

@hookimpl
def djadmin_get_action_view_mixins(action):
    """Add version tracking to create/update actions"""
    from djadmin.actions.view_mixins import CreateViewActionMixin, UpdateViewActionMixin
    from .mixins import VersionTrackingMixin

    return {
        CreateViewActionMixin: [VersionTrackingMixin],
        UpdateViewActionMixin: [VersionTrackingMixin],
    }

Version Tracking Mixin

# djadmin_versions/mixins.py
class VersionTrackingMixin:
    """Save version after successful save"""

    def form_valid(self, form):
        # Save the form
        response = super().form_valid(form)

        # Create version snapshot
        from .models import Version
        Version.create_from_object(self.object, self.request.user)

        return response

Version History Action

# djadmin_versions/actions.py
from djadmin.actions import BaseAction, RecordActionMixin
from django.views.generic import TemplateView

class VersionHistoryAction(RecordActionMixin, BaseAction):
    label = 'Version History'
    icon = 'history'

    # Use TemplateView as base for display-only action
    base_class = TemplateView
    template_name = 'djadmin_versions/history.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        from .models import Version
        obj = self.get_object()
        versions = Version.objects.filter(
            content_type__model=obj._meta.model_name,
            object_id=obj.pk
        ).order_by('-created_at')

        context.update({
            'object': obj,
            'versions': versions,
        })
        return context

Extension: Use django-reversion, diff view, rollback action, compare versions.


Best Practices Summary

  1. Single Responsibility: Each plugin provides one feature
  2. Feature Advertising: Always declare features with djadmin_provides_features()
  3. Composability: Plugins should work independently and together
  4. Configuration: Use ModelAdmin attributes for plugin settings
  5. Documentation: Include README with installation and configuration instructions
  6. Testing: Test plugin hooks and feature integration
  7. Dependencies: Document required packages and django-admin-deux features

Next Steps