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:
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.
Built-in Approach (Recommended)¶
# 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¶
- Single Responsibility: Each plugin provides one feature
- Feature Advertising: Always declare features with
djadmin_provides_features() - Composability: Plugins should work independently and together
- Configuration: Use
ModelAdminattributes for plugin settings - Documentation: Include README with installation and configuration instructions
- Testing: Test plugin hooks and feature integration
- Dependencies: Document required packages and django-admin-deux features
Next Steps¶
- Creating Plugins - Build your own plugin
- Hook Reference - Complete hook documentation
- Custom Actions - Action patterns
- Built-in plugins:
djadmin/plugins/