Layout Integration Guide - djadmin-formset¶
Plugin: djadmin-formset
Version: 0.1.0 (Alpha)
Last Updated: October 21, 2025
Overview¶
The djadmin-formset plugin seamlessly integrates with the django-admin-deux Layout API to provide advanced form features powered by django-formset. When installed, the plugin automatically enhances ALL forms with FormCollection support.
How It Works¶
Progressive Enhancement Architecture¶
The Layout API in django-admin-deux is designed with progressive enhancement in mind:
┌─────────────────────────────────────────────┐
│ Core (djadmin) │
│ - Layout API (Field, Fieldset, Row, etc.)│
│ - Basic form rendering │
│ - Flexbox layout support │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Plugin (djadmin-formset) - OPTIONAL │
│ - FormCollection conversion │
│ - Inline editing │
│ - Conditional fields │
│ - Computed fields │
│ - Client-side validation │
│ - Drag-and-drop ordering │
└─────────────────────────────────────────────┘
Without plugin: Forms work with basic rendering (fieldsets, rows with flexbox) With plugin: ALL forms are enhanced with django-formset features
Automatic Form Conversion¶
The plugin uses the djadmin_get_action_view_mixins hook to inject the DjAdminFormsetMixin into all form-based actions:
# djadmin_formset/djadmin_hooks.py
@hookimpl
def djadmin_get_action_view_mixins():
"""Provide mixins for form actions."""
from djadmin_formset.mixins import DjAdminFormsetMixin
from djadmin.actions import FormViewActionMixin
return {
FormViewActionMixin: [DjAdminFormsetMixin]
}
The mixin overrides get_form_class() to use FormFactory instead of core's FormBuilder:
# djadmin_formset/mixins.py
class DjAdminFormsetMixin:
"""Mixin that converts layouts to FormCollections."""
def get_form_class(self):
"""Override to use FormFactory instead of FormBuilder."""
layout = self.get_layout()
if not layout:
return super().get_form_class()
from djadmin_formset.factories import FormFactory
return FormFactory.from_layout(
layout=layout,
model=self.model,
base_form=self.get_base_form_class(),
renderer=layout.renderer or self.get_default_renderer(),
)
Installation¶
Basic Installation¶
# Install the plugin
pip install djadmin-formset
# Or install with django-admin-deux
pip install django-admin-deux[djadmin-formset]
Django Settings¶
Add to INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
# Django apps
'django.contrib.contenttypes',
'django.contrib.auth',
# ...
# Django-admin-deux (core)
'djadmin',
# Plugin - MUST come after djadmin
'djadmin_formset',
# Your apps
'myapp',
]
CRITICAL: djadmin_formset must come after djadmin in INSTALLED_APPS.
Verify Installation¶
Check that the plugin is loaded:
# Django shell
from djadmin.plugins import pm
# Check registered plugins
plugins = pm.list_name_plugin()
print([name for name, plugin in plugins])
# Should include: 'djadmin_formset'
# Check provided features
features = pm.hook.djadmin_provides_features()
print(features)
# Should include: ['collections', 'inlines', 'conditional_fields', 'computed_fields']
Usage¶
Basic Layout (Works Without Plugin)¶
from djadmin import ModelAdmin, register, Layout, Field, Fieldset, Row
@register(Author)
class AuthorAdmin(ModelAdmin):
"""Basic layout - works with or without plugin."""
layout = Layout(
Fieldset('Personal Information',
Row(
Field('first_name', css_classes=['flex-1', 'pr-2']),
Field('last_name', css_classes=['flex-1', 'pl-2']),
),
Field('birth_date', label='Date of Birth'),
),
Fieldset('Biography',
Field('bio', widget='textarea', attrs={'rows': 8}),
),
)
Without plugin: Renders with basic flexbox layout and fieldsets With plugin: Enhanced with FormCollection, client-side validation, better styling
Advanced Features (Require Plugin)¶
Once the plugin is installed, you can use advanced features:
from djadmin import ModelAdmin, register, Layout, Field, Collection
@register(Author)
class AuthorAdmin(ModelAdmin):
"""Advanced layout - requires plugin."""
layout = Layout(
Field('name'),
Field('birth_date'),
# Collection component - requires plugin
Collection('books',
model=Book,
fields=['title', 'isbn', 'published_date'],
is_sortable=True,
min_siblings=0,
max_siblings=10,
),
)
Without plugin: Raises ImproperlyConfigured at startup with installation instructions
With plugin: Full inline editing with drag-and-drop ordering
Feature Detection¶
The plugin advertises its capabilities via the djadmin_provides_features hook:
@hookimpl
def djadmin_provides_features():
"""Advertise features provided by this plugin."""
return [
'collections', # Inline editing support
'inlines', # Alias for collections
'conditional_fields', # Show/hide field logic
'computed_fields', # Auto-calculated fields
]
Core validates features at startup. If a layout uses features not provided by any plugin, it raises a clear error:
ImproperlyConfigured: Form for Author requires features that are not available:
• Inline editing (Collection components) requires the djadmin-formset plugin.
Install with: pip install django-admin-deux[djadmin-formset]
Auto-Layout Creation¶
Every ModelAdmin automatically has a layout attribute:
The metaclass creates a basic auto-layout:
layout = Layout(
*[Field(field_name) for field_name in model_fields]
)
layout._layout_source = 'auto'
This ensures the plugin can always convert forms to FormCollections, even when no explicit layout is defined.
Layout Sources¶
Layouts can come from three sources:
-
Explicit Layout:
-
Converted from Fieldsets:
-
Auto-Generated:
The plugin works with all three sources.
Action-Specific Layouts¶
The plugin respects action-specific layouts:
class ProductAdmin(ModelAdmin):
"""Different layouts for create vs update."""
# Generic layout (fallback)
layout = Layout(
Field('name'),
Field('price'),
)
# Create-specific layout
create_layout = Layout(
Fieldset('New Product',
Field('name', required=True),
Field('sku'),
Field('price'),
),
)
# Update-specific layout
update_layout = Layout(
Fieldset('Product Information',
Field('name'),
Field('sku', widget='text', attrs={'readonly': True}),
Field('price'),
),
Fieldset('Metadata',
Field('created_at', widget='text', attrs={'readonly': True}),
Field('updated_at', widget='text', attrs={'readonly': True}),
),
)
How it works:
- AddAction uses create_layout or falls back to layout
- EditAction uses update_layout or falls back to layout
- Plugin converts the action-specific layout to FormCollection
FormFactory Architecture¶
The plugin uses FormFactory to convert layouts to django-formset FormCollection classes:
# Simplified example
from djadmin_formset.factories import FormFactory
# Convert layout to FormCollection
FormCollectionClass = FormFactory.from_layout(
layout=author_admin.layout,
model=Author,
base_form=None, # Uses formset's ModelForm
renderer=DjAdminFormRenderer,
)
# Instantiate with data
form_collection = FormCollectionClass(
data=request.POST,
instance=author_instance,
)
Main Form Pattern¶
CRITICAL: django-formset's FormCollection can only hold Form instances, not individual fields.
The plugin uses the "main form" pattern:
FormCollection
├── 'main' → ModelForm (contains all simple Field components)
├── 'books' → NestedFormCollection (for Collection components)
│ ├── 'main' → ModelForm (contains collection fields)
└── 'addresses' → NestedFormCollection
├── 'main' → ModelForm
Why? django-formset's declared_holders expects Form instances, not bare fields.
Field Access in Tests¶
When testing FormCollections created by the plugin:
# Access fields via the 'main' form
form = FormCollectionClass()
field = form.declared_holders['main'].fields['field_name']
# For nested collections
collection = form.declared_holders['books']
nested_field = collection.declared_holders['main'].fields['title']
Renderer Customization¶
The plugin provides DjAdminFormRenderer with Tailwind CSS classes matching the djadmin theme:
from djadmin import ModelAdmin, register, Layout
from djadmin_formset.renderers import DjAdminFormRenderer
@register(Author)
class AuthorAdmin(ModelAdmin):
layout = Layout(
# ... fields ...
renderer=DjAdminFormRenderer, # Explicit renderer
)
See Renderer Customization for details on creating custom renderers.
Template Overrides¶
The plugin provides template overrides for form actions:
djadmin_formset/templates/djadmin/
├── actions/
│ ├── add.html # Includes django-formset JS/CSS
│ └── edit.html # Includes django-formset JS/CSS
These templates: - Include django-formset JavaScript and CSS - Set up FormCollection rendering - Work seamlessly with the core djadmin theme
Troubleshooting¶
Plugin Not Loading¶
Symptom: Collections show warnings, conditional fields don't work
Solution: Check INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
'djadmin', # MUST come first
'djadmin_formset', # MUST come after djadmin
]
Feature Validation Errors¶
Symptom: ImproperlyConfigured on startup
Example:
ImproperlyConfigured: Form for Author requires features that are not available:
• Inline editing (Collection components) requires the djadmin-formset plugin.
Solution: Install the plugin:
Forms Not Converting to FormCollections¶
Symptom: Forms render with basic HTML, no enhanced features
Check:
1. Plugin installed and in INSTALLED_APPS?
2. Layout defined on ModelAdmin?
3. Check logs for errors
Debug:
# Django shell
from djadmin import site
from myapp.models import Author
admin = site.get_model_admin(Author)
print(hasattr(admin, 'layout')) # Should be True
print(admin.layout._layout_source) # 'layout', 'fieldsets', or 'auto'
# Check if mixin is applied
from myapp.djadmin import AuthorAdmin
from djadmin.actions import AddAction
action = AddAction(admin)
print(action.__class__.__mro__) # Should include DjAdminFormsetMixin
JavaScript Not Loading¶
Symptom: Conditional fields, computed fields don't work
Check: View page source, look for:
Solution: Ensure django-formset static files are collected:
Next Steps¶
- Inline Editing Guide - Collections and nested forms
- Conditional Fields Guide - Show/hide logic
- Computed Fields Guide - Auto-calculated values
- Renderer Customization - Custom styling