Creating Plugins¶
This guide walks you through creating a custom plugin for django-admin-deux.
Quick Start¶
1. Create a Django App¶
Your plugin is a standard Django app:
Add it to INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
# ...
'djadmin',
'myfeature', # Your plugin
]
2. Create the Plugin Module¶
Create myfeature/djadmin_hooks.py:
from djadmin.plugins import hookimpl
@hookimpl
def djadmin_provides_features():
"""Advertise features this plugin provides"""
return ['myfeature']
That's it! Your plugin is now discovered automatically.
Example: Export Plugin¶
Let's build a real plugin that adds CSV export functionality.
Step 1: Define the Feature¶
# myfeature/djadmin_hooks.py
from djadmin.plugins import hookimpl
@hookimpl
def djadmin_provides_features():
return ['export']
Step 2: Create the Action¶
# myfeature/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 execute(self, request, **kwargs):
# Get all records
queryset = self.model.objects.all()
# Create CSV response
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{self.model._meta.model_name}.csv"'
writer = csv.writer(response)
# Write header
fields = [f.name for f in self.model._meta.fields]
writer.writerow(fields)
# Write data
for obj in queryset:
writer.writerow([getattr(obj, f) for f in fields])
return response
Step 3: Register the Action¶
# myfeature/djadmin_hooks.py
from djadmin.plugins import hookimpl
@hookimpl
def djadmin_provides_features():
return ['export']
@hookimpl
def djadmin_get_default_general_actions():
"""Add export action to all ListViews"""
from .actions import ExportCSVAction
return [ExportCSVAction]
That's it! The export button now appears on all list views.
Hook Implementation Patterns¶
Providing Default Actions¶
@hookimpl
def djadmin_get_default_general_actions():
from .actions import MyListAction
return [MyListAction]
@hookimpl
def djadmin_get_default_bulk_actions():
from .actions import MyBulkAction
return [MyBulkAction]
@hookimpl
def djadmin_get_default_record_actions():
from .actions import MyRecordAction
return [MyRecordAction]
Adding View Mixins¶
@hookimpl
def djadmin_get_action_view_mixins(action):
"""Add mixins to specific action types"""
from djadmin.actions.view_mixins import ListActionMixin
from .mixins import SearchMixin
return {
ListActionMixin: [SearchMixin],
}
Injecting View Attributes¶
@hookimpl
def djadmin_get_action_view_attributes(action):
"""Add attributes to generated views"""
from djadmin.actions.view_mixins import ListActionMixin
return {
ListActionMixin: {
'paginate_by': action.model_admin.paginate_by,
}
}
Contributing Assets¶
@hookimpl
def djadmin_get_action_view_assets(action):
"""Add CSS/JS to views"""
from djadmin import JSAsset, CSSAsset
from djadmin.actions.base import BaseAction
return {
BaseAction: {
'css': [CSSAsset(href='myfeature/css/styles.css')],
'js': [
# Critical script that must load before rendering (e.g., web components)
JSAsset(src='myfeature/js/critical.js', blocking=True),
# Non-critical enhancement - defer for better performance
JSAsset(src='myfeature/js/feature.js', defer=True),
],
}
}
When to use blocking=True:
- Critical polyfills needed before other scripts run
- Non-module scripts that must execute before rendering
- NOT for ES modules - module=True scripts are always deferred by browser spec
Performance tip: Most JavaScript should use defer=True for optimal page load performance. Only use blocking=True for non-module scripts when absolutely necessary.
Modifying Querysets¶
@hookimpl
def djadmin_modify_queryset(queryset, request, view):
"""Filter or annotate querysets"""
# Example: Add search filtering
if 'q' in request.GET:
search_term = request.GET['q']
return queryset.filter(title__icontains=search_term)
return queryset
Adding Context Data¶
@hookimpl
def djadmin_add_context_data(context, request, view):
"""Add extra context to templates"""
return {
'my_feature_enabled': True,
'feature_config': {'option': 'value'},
}
Feature-Driven Development¶
Use feature indicators in ModelAdmin to trigger plugin requirements:
# settings.py - ModelAdmin configuration
class BookAdmin(ModelAdmin):
list_display = ['title', 'author']
search_fields = ['title'] # Requires 'search' feature
If no plugin provides 'search', django-admin-deux raises an error at startup.
Feature indicators are attributes that signal feature requirements:
- search_fields → requires 'search' feature
- list_filter → requires 'filter' feature
- ordering → requires 'ordering' feature
- inlines → requires 'inlines' feature (future)
See djadmin/options.py (ModelAdmin.FEATURE_INDICATORS) for the complete mapping.
Plugin Structure¶
Recommended organization:
myfeature/
├── __init__.py
├── apps.py # Django app config
├── djadmin_hooks.py # Hook implementations (required)
├── actions.py # Custom actions
├── mixins.py # View mixins
├── templates/
│ └── djadmin/
│ └── myfeature/ # Feature templates
└── static/
└── myfeature/ # CSS/JS assets
├── css/
└── js/
Testing Your Plugin¶
# myfeature/tests/test_plugin.py
import pytest
from djadmin.plugins import pm
def test_plugin_discovered():
"""Test that plugin is discovered"""
features = pm.hook.djadmin_provides_features()
assert 'myfeature' in features
def test_action_registered():
"""Test that actions are registered"""
actions = pm.hook.djadmin_get_default_general_actions()
from myfeature.actions import MyAction
assert any(isinstance(a, MyAction) for action_list in actions for a in action_list)
Advanced Patterns¶
Conditional Plugin Behavior¶
@hookimpl
def djadmin_get_default_general_actions():
"""Only provide actions if dependencies are available"""
try:
import pandas
except ImportError:
return []
from .actions import ExportExcelAction
return [ExportExcelAction]
Plugin Configuration¶
# myfeature/djadmin_hooks.py
from django.conf import settings
@hookimpl
def djadmin_add_context_data(context, request, view):
config = getattr(settings, 'MYFEATURE_CONFIG', {})
return {'myfeature_config': config}
Plugin Dependencies¶
Document plugin dependencies in your app's README.md:
## Requirements
- django-admin-deux >= 0.1.0
- Requires 'crud' feature (provided by core plugin)
- Optional: 'theme' feature for styled UI
Distribution¶
Package your plugin as a standard Django app:
# setup.py
from setuptools import setup
setup(
name='djadmin-myfeature',
packages=['myfeature'],
install_requires=[
'django-admin-deux>=0.1.0',
],
)
Users install it like any Django app:
Then add to INSTALLED_APPS:
Next Steps¶
- Hook Reference - Complete hook documentation
- Custom Actions - Deep dive into action patterns
- Examples - Real-world plugin examples
- Core plugin source:
djadmin/plugins/core/djadmin_hooks.py - Theme plugin source:
djadmin/plugins/theme/djadmin_hooks.py