Plugin-Driven INSTALLED_APPS¶
Status: Implemented
Function: djadmin.djadmin_apps()
Related: Milestone 4 - Developer Experience
Overview¶
djadmin_apps() provides zero-configuration INSTALLED_APPS setup for django-admin-deux and its plugins. Instead of manually managing app lists and ordering, plugins declare their dependencies via hooks, and djadmin_apps() automatically resolves the correct order.
Quick Start¶
# settings.py
from djadmin import djadmin_apps
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Your apps
'myapp',
'anotherapp',
] + djadmin_apps() # Automatically adds djadmin + all plugins
That's it! No manual plugin configuration needed.
What It Does¶
djadmin_apps() performs these steps:
- Discovers third-party plugins via Python entrypoints (
project.entry-points.djadmin) - Registers built-in plugins (core and theme) explicitly
- Calls plugin hooks to get required apps (
djadmin_get_required_apps()) - Resolves ordering using
First,Before,After,Positionmodifiers - Removes duplicates while preserving order
- Returns ordered list of app names
How Plugins Declare Apps¶
Built-in Plugins¶
Built-in plugins (core, theme) are explicitly registered in djadmin_apps() and implement the hook:
Core Plugin (djadmin/plugins/core/djadmin_hooks.py):
from djadmin.plugins import hookimpl
@hookimpl
def djadmin_get_required_apps():
"""Core plugin apps."""
return [
'djadmin.plugins.core',
]
Theme Plugin (djadmin/plugins/theme/djadmin_hooks.py):
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import First
@hookimpl
def djadmin_get_required_apps():
"""Theme plugin must load first for template precedence."""
return [
First('djadmin.plugins.theme'),
]
Third-Party Plugins¶
Third-party plugins declare themselves via entrypoints in pyproject.toml:
Step 1: Declare entrypoint
# your_plugin/pyproject.toml
[project.entry-points.djadmin]
your_plugin = "your_plugin.djadmin_hooks"
Step 2: Implement hook
# your_plugin/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import After
@hookimpl
def djadmin_get_required_apps():
"""Required apps for your plugin."""
return [
After('your_plugin'), # Load after core apps
'dependency_app', # Additional dependency (no ordering)
]
Step 3: Install plugin
That's it! djadmin_apps() will discover your plugin automatically.
Ordering Modifiers¶
Plugins use modifiers to control app ordering:
First(app_name)¶
Places app at the very beginning of the list.
Use case: Themes that need template precedence
from djadmin.plugins.modifiers import First
@hookimpl
def djadmin_get_required_apps():
return [
First('my_theme'), # Must load first
]
Result:
Before(app_name)¶
Places app in the "before" bucket, which loads before core apps.
Use case: Dependencies that djadmin relies on
from djadmin.plugins.modifiers import Before
@hookimpl
def djadmin_get_required_apps():
return [
Before('formset'), # formset must load before djadmin
]
Result:
After(app_name)¶
Places app in the "after" bucket, which loads after core apps.
Use case: Plugins that extend djadmin features
from djadmin.plugins.modifiers import After
@hookimpl
def djadmin_get_required_apps():
return [
After('my_plugin'), # Load after core
]
Result:
Position(app_name, before=None, after=None)¶
Places app relative to another specific app.
Use case: Complex ordering constraints
from djadmin.plugins.modifiers import Position
@hookimpl
def djadmin_get_required_apps():
return [
Position('my_plugin', after='some_other_plugin'),
]
Result:
Note: Both before and after can be specified for precise positioning.
No Modifier (Default Bucket)¶
Apps without modifiers go in the "default" bucket with no ordering constraints.
Ordering Resolution Algorithm¶
djadmin_apps() uses a bucket-based algorithm:
# 1. Categorize apps into buckets
first = [] # First('app')
before = [] # Before('app')
default = [] # Plain 'app' or djadmin core
after = [] # After('app')
position = [] # Position('app', before/after='...')
# 2. Combine buckets
combined = first + before + default + after
# 3. Insert Position apps relative to existing apps
for pos_app in position:
find_position_in_combined(pos_app)
insert_at_correct_position(pos_app)
# 4. Remove duplicates (preserves first occurrence)
unique_apps = remove_duplicates(combined)
# 5. Return final list
return unique_apps
Examples¶
Example 1: Theme Plugin¶
A theme plugin needs to load first for template override:
# my_theme/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import First
@hookimpl
def djadmin_get_required_apps():
return [
First('my_theme'), # Template precedence
]
Entrypoint:
Result in INSTALLED_APPS:
[
# ... Django apps ...
'my_theme', # First (theme)
'djadmin.plugins.theme', # First (default theme) - duplicate removed
'djadmin', # Core
'djadmin.plugins.core', # Core plugin
]
Example 2: Formset Plugin (Template Override Pattern)¶
A plugin that needs to override templates must load BEFORE djadmin core.
CRITICAL: Plugins with templates must use Before() to ensure Django's template loader finds their templates first.
# djadmin_formset/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import Before
@hookimpl
def djadmin_get_required_apps():
"""
CRITICAL: djadmin_formset must load BEFORE djadmin to override templates.
Django's template loader uses first-match resolution, so our templates in
djadmin_formset/templates/djadmin/actions/edit.html must be found before
djadmin/templates/djadmin/actions/edit.html.
"""
return [
Before('formset'), # django-formset needs early loading
Before('djadmin_formset'), # Our plugin app - MUST be before djadmin for template overrides
]
Entrypoint:
Result:
[
# ... Django apps ...
'djadmin.plugins.theme', # First (theme template overrides)
'formset', # Before bucket (dependency)
'djadmin_formset', # Before bucket (CRITICAL for template overrides!)
'djadmin', # Core (templates loaded after plugins)
'djadmin.plugins.core', # Core plugin
]
Why This Matters:
- Django's APP_DIRS template loader searches through INSTALLED_APPS in order
- Both djadmin and djadmin_formset have templates at djadmin/actions/edit.html
- djadmin_formset version uses <django-formset> web components
- djadmin version uses standard <form> tags
- Without Before(), the core template loads first, breaking FormCollection rendering
Example 3: Filter Plugin¶
A plugin with no special ordering requirements:
# djadmin_filters/djadmin_hooks.py
from djadmin.plugins import hookimpl
@hookimpl
def djadmin_get_required_apps():
return [
'django_filters', # No ordering constraint
]
Entrypoint:
Result:
[
# ... Django apps ...
'djadmin', # Core
'djadmin.plugins.core', # Core plugin
'django_filters', # Default bucket (no constraint)
]
Example 4: Complex Positioning¶
A plugin that must load after another plugin:
# enhanced_filters/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import Position
@hookimpl
def djadmin_get_required_apps():
return [
Position('enhanced_filters', after='django_filters'),
]
Result:
[
# ... Django apps ...
'djadmin',
'django_filters', # Base filters
'enhanced_filters', # After django_filters
]
Plugin Discovery¶
How Discovery Works¶
At settings import time (before Django starts):
djadmin_apps()is called fromsettings.py- Built-in plugins (core, theme) are explicitly registered:
- Third-party plugins discovered via entrypoints:
- All plugins call
djadmin_get_required_apps()hook - Apps collected and ordered
Why Entrypoints?¶
The Chicken-and-Egg Problem:
- djadmin_apps() runs in settings.py (before Django starts)
- Traditional plugin discovery happens in AppConfig.ready() (after Django starts)
- Can't discover plugins after apps are loaded
The Solution:
- Python entrypoints are available at import time
- Standard packaging mechanism (setuptools, pip)
- Discoverable before Django starts
- Built-in plugins registered explicitly (no entrypoint needed)
Entrypoint Format¶
Requirements:
- Entry point group: djadmin
- Entry point name: Any unique name (conventionally: package name)
- Entry point value: Dotted path to djadmin_hooks module
Example:
Django will load my_plugin.djadmin_hooks and register it with the plugin manager.
Usage Patterns¶
Standard Setup¶
# settings.py
from djadmin import djadmin_apps
INSTALLED_APPS = [
'django.contrib.admin',
# ... standard Django apps ...
'myapp',
] + djadmin_apps()
With Custom Apps List¶
# settings.py
from djadmin import djadmin_apps
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
# ...
]
THIRD_PARTY_APPS = [
'rest_framework',
'crispy_forms',
]
LOCAL_APPS = [
'myapp',
'anotherapp',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + djadmin_apps()
Conditional Inclusion¶
# settings.py
from djadmin import djadmin_apps
INSTALLED_APPS = [
'django.contrib.admin',
# ...
]
# Only use djadmin in development
if DEBUG:
INSTALLED_APPS += djadmin_apps()
Troubleshooting¶
ImproperlyConfigured: Could not position app¶
Error:
ImproperlyConfigured: Could not position 'my_app' after 'other_app':
'other_app' not found in combined apps.
Cause: Position(my_app, after='other_app') but other_app isn't in the list.
Solutions:
1. Check other_app is provided by a plugin hook
2. Verify other_app spelling is correct
3. Use After() instead if relative positioning isn't critical
Plugin not discovered¶
Symptoms: Plugin apps not in INSTALLED_APPS
Check:
1. Entrypoint declared correctly in pyproject.toml:
python -c "from importlib.metadata import entry_points; print([ep for ep in entry_points(group='djadmin')])"
-
Plugin installed:
-
Hook module exists:
-
Hook implemented:
Duplicate apps¶
Symptoms: Same app appears multiple times in INSTALLED_APPS
Cause: You added the app manually AND plugin provides it
Solution: Let djadmin_apps() handle it - remove manual entry:
# ❌ BAD - Duplicates
INSTALLED_APPS = [
'django_filters', # Manual
# ...
] + djadmin_apps() # Also provides django_filters
# ✅ GOOD - No duplicates
INSTALLED_APPS = [
# ... Django apps only
] + djadmin_apps() # Provides django_filters automatically
Order is wrong¶
Symptoms: Template override not working, app loading in wrong order
Debug: 1. Print the result:
- Check modifier usage:
- Use
First()for themes (template precedence) - Use
Before()for dependencies (load before djadmin) -
Use
After()for extensions (load after djadmin) -
Verify plugin hook is called:
Best Practices¶
DO: Use modifiers appropriately¶
# Theme plugin - needs First
First('my_theme')
# Dependency - needs Before
Before('formset')
# Extension - use After
After('my_plugin')
# No special needs - no modifier
'django_filters'
DO: Keep dependencies minimal¶
# ✅ GOOD - Only essential deps
@hookimpl
def djadmin_get_required_apps():
return [
'required_lib',
]
# ❌ BAD - Unnecessary deps
@hookimpl
def djadmin_get_required_apps():
return [
'optional_lib', # Should be user's choice
'test_lib', # Test dependency, not runtime
]
DO: Document why modifiers are needed¶
@hookimpl
def djadmin_get_required_apps():
"""
Required apps for formset integration.
- formset: Must load before djadmin (Before) because djadmin
imports formset classes during view generation.
"""
return [
Before('formset'),
]
DON'T: Use Position() unless necessary¶
# ❌ AVOID - Position is complex
Position('my_app', after='specific_app')
# ✅ PREFER - Use buckets when possible
After('my_app')
DON'T: Return djadmin core¶
# ❌ BAD - djadmin_apps() adds 'djadmin' automatically
@hookimpl
def djadmin_get_required_apps():
return ['djadmin', 'my_plugin']
# ✅ GOOD - Only return your plugin's apps
@hookimpl
def djadmin_get_required_apps():
return ['my_plugin']
DON'T: Modify INSTALLED_APPS manually after djadmin_apps()¶
# ❌ BAD - Order can be broken
INSTALLED_APPS += djadmin_apps()
INSTALLED_APPS.insert(0, 'my_theme') # Might break ordering
# ✅ GOOD - Let djadmin_apps() handle ordering
INSTALLED_APPS += djadmin_apps()
Advanced Usage¶
Conditional Apps¶
# your_plugin/djadmin_hooks.py
from djadmin.plugins import hookimpl
from django.conf import settings
@hookimpl
def djadmin_get_required_apps():
"""Conditionally include apps based on settings."""
apps = ['my_plugin']
# Add optional feature app if enabled
if getattr(settings, 'MY_PLUGIN_FEATURE_X', False):
apps.append('my_plugin.feature_x')
return apps
Debug Mode¶
# settings.py
from djadmin import djadmin_apps
# Show what djadmin_apps() would add
if DEBUG:
apps = djadmin_apps()
print("djadmin_apps() adding:", apps)
INSTALLED_APPS = [
# ...
] + apps
Testing Without djadmin_apps()¶
# tests/settings.py
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'djadmin',
'djadmin.plugins.core',
# Manually specify for testing
]
Performance¶
Discovery time: <50ms - Entrypoint loading: ~20ms - Hook calls: ~10ms - Ordering resolution: ~20ms
Caching: Not cached (runs once at settings import)
Impact: Negligible (<100ms added to Django startup)
Migration Guide¶
From Manual INSTALLED_APPS¶
Before:
INSTALLED_APPS = [
'django.contrib.admin',
# ...
'djadmin',
'djadmin.plugins.core',
'djadmin.plugins.theme',
'formset',
'django_filters',
'my_plugin',
]
After:
from djadmin import djadmin_apps
INSTALLED_APPS = [
'django.contrib.admin',
# ... your apps only
] + djadmin_apps()
From Old Plugin Format¶
If your plugin previously required manual INSTALLED_APPS setup:
Before (in docs):
After (in code):
# my_plugin/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.plugins.modifiers import Before
@hookimpl
def djadmin_get_required_apps():
return [
Before('formset'),
'my_plugin',
]