Skip to content

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:

python manage.py startapp myfeature

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:

pip install djadmin-myfeature

Then add to INSTALLED_APPS:

INSTALLED_APPS = [
    'djadmin',
    'myfeature',
]

Next Steps