Skip to content

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:

class MyModelAdmin(ModelAdmin):
    # No layout specified
    pass

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:

  1. Explicit Layout:

    class MyAdmin(ModelAdmin):
        layout = Layout(Field('name'), Field('email'))
        # _layout_source = 'layout'
    

  2. Converted from Fieldsets:

    class MyAdmin(ModelAdmin):
        fieldsets = (
            ('Personal', {'fields': ('name', 'email')}),
        )
        # Metaclass converts to layout
        # _layout_source = 'fieldsets'
    

  3. Auto-Generated:

    class MyAdmin(ModelAdmin):
        # No layout or fieldsets
        pass
        # Metaclass creates auto-layout from model fields
        # _layout_source = 'auto'
    

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:

pip install djadmin-formset

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:

<script src="/static/formset/js/django-formset.min.js"></script>

Solution: Ensure django-formset static files are collected:

python manage.py collectstatic

Next Steps

See Also