Skip to content

Django Admin Migration Guide

This guide helps you migrate existing Django admin code to django-admin-deux's Layout API.

Quick Summary

Good news: You can often just copy your existing fieldsets code!

# Old Django admin - works in djadmin too!
class BookAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Personal', {
            'fields': ('name', ('first_name', 'last_name'))
        }),
    )

# New djadmin - SAME CODE!
class BookAdmin(ModelAdmin):
    fieldsets = (  # ← Automatically converts to Layout
        ('Personal', {
            'fields': ('name', ('first_name', 'last_name'))
        }),
    )

The ModelAdmin metaclass automatically converts fieldsets to Layout at class creation time.

Table of Contents


Why Migrate?

Limitations of Django Admin

  1. Rigid Syntax - Tuples are hard to read and maintain
  2. No Type Safety - Easy to make mistakes
  3. Limited Layouts - Only vertical stacking
  4. No Conditionals - Can't show/hide fields dynamically
  5. Inline Limitations - Limited control over inline appearance

Advantages of Layout API

  1. Declarative - Clean, readable Python syntax
  2. Type-safe - Dataclasses with validation
  3. Flexible - Fieldsets, rows, and nested layouts
  4. Progressive - Start simple, add features as needed
  5. Extensible - Plugin system for advanced features
  6. Compatible - Automatic conversion from Django admin

Automatic Conversion

How It Works

The ModelAdminMetaclass automatically converts fieldsets to Layout:

class ModelAdminMetaclass(type):
    def __new__(mcs, name, bases, namespace):
        # Skip base class
        if name == 'ModelAdmin':
            return super().__new__(mcs, name, bases, namespace)

        # Check for both fieldsets and layout
        has_fieldsets = 'fieldsets' in namespace
        has_layout = 'layout' in namespace

        if has_fieldsets and has_layout:
            raise ImproperlyConfigured(
                f"{name} cannot specify both 'fieldsets' and 'layout'"
            )

        # Auto-convert fieldsets to layout
        if has_fieldsets:
            from djadmin.layout import Layout
            fieldsets = namespace.pop('fieldsets')
            namespace['layout'] = Layout.from_fieldsets(fieldsets)
            namespace['_layout_source'] = 'fieldsets'

        return super().__new__(mcs, name, bases, namespace)

Conversion Rules

Django Admin Syntax Layout API Equivalent
('field1', 'field2') Row(Field('field1'), Field('field2'))
'field1' Field('field1')
(None, {'fields': ...}) Fieldset(None, ...)
('Legend', {'fields': ...}) Fieldset('Legend', ...)
'classes': ['collapse'] css_classes=['collapse']
'description': 'text' description='text'

What You Can Copy

Can copy directly: - fieldsets - Entire fieldsets definition - Named fieldsets with legends - Unnamed fieldsets (None legend) - Tuple syntax for horizontal fields - classes options - description text

Cannot auto-convert: - fields - Use layout instead - exclude - Not supported - readonly_fields - Use Field(widget='...') instead - inlines - Use Collection instead (manual migration)


Manual Conversion

Step-by-Step Process

1. Simple Fields List

Django Admin:

class ArticleAdmin(admin.ModelAdmin):
    fields = ['title', 'content', 'published_date']

Layout API:

class ArticleAdmin(ModelAdmin):
    layout = Layout(
        Field('title'),
        Field('content'),
        Field('published_date'),
    )

2. Fieldsets with Single Fields

Django Admin:

class BookAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Book Information', {
            'fields': ('title', 'author', 'isbn'),
        }),
        ('Publishing', {
            'fields': ('publisher', 'published_date'),
        }),
    )

Layout API (automatic conversion works, or manual):

class BookAdmin(ModelAdmin):
    # Option 1: Just copy (automatic conversion)
    fieldsets = (
        ('Book Information', {
            'fields': ('title', 'author', 'isbn'),
        }),
        ('Publishing', {
            'fields': ('publisher', 'published_date'),
        }),
    )

    # Option 2: Manual Layout (more control)
    layout = Layout(
        Fieldset('Book Information',
            Field('title'),
            Field('author'),
            Field('isbn'),
        ),
        Fieldset('Publishing',
            Field('publisher'),
            Field('published_date'),
        ),
    )

3. Horizontal Field Groups

Django Admin:

class PersonAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Name', {
            'fields': (('first_name', 'last_name'),),
        }),
        ('Contact', {
            'fields': ('email', ('phone', 'mobile')),
        }),
    )

Layout API:

class PersonAdmin(ModelAdmin):
    layout = Layout(
        Fieldset('Name',
            Row(
                Field('first_name', css_classes=['flex-1', 'pr-2']),
                Field('last_name', css_classes=['flex-1', 'pl-2']),
            ),
        ),
        Fieldset('Contact',
            Field('email'),
            Row(
                Field('phone', css_classes=['flex-1', 'pr-2']),
                Field('mobile', css_classes=['flex-1', 'pl-2']),
            ),
        ),
    )

Note: Use css_classes for flex layout to control width distribution.

4. Fieldsets with Classes and Description

Django Admin:

class AdvancedAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Advanced Options', {
            'classes': ('collapse',),
            'description': 'These options are for advanced users',
            'fields': ('debug_mode', 'verbose_logging'),
        }),
    )

Layout API:

class AdvancedAdmin(ModelAdmin):
    layout = Layout(
        Fieldset('Advanced Options',
            Field('debug_mode'),
            Field('verbose_logging'),
            css_classes=['collapse'],
            description='These options are for advanced users',
        ),
    )

5. Unnamed Fieldsets

Django Admin:

class ProductAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {
            'fields': ('name', 'sku', 'price'),
        }),
        ('Inventory', {
            'fields': ('stock', 'reorder_level'),
        }),
    )

Layout API:

class ProductAdmin(ModelAdmin):
    layout = Layout(
        Fieldset(None,  # Unnamed fieldset
            Field('name'),
            Field('sku'),
            Field('price'),
        ),
        Fieldset('Inventory',
            Field('stock'),
            Field('reorder_level'),
        ),
    )


Inlines Migration

Django admin inlines → Collection components (requires plugin).

TabularInline → Collection with fields

Django Admin:

class BookInline(admin.TabularInline):
    model = Book
    fields = ['title', 'isbn', 'published_date']
    extra = 1
    max_num = 10

class AuthorAdmin(admin.ModelAdmin):
    inlines = [BookInline]

Layout API ⚠️ Requires djadmin-formset plugin:

class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('birth_date'),
        Collection('books',
            model=Book,
            fields=['title', 'isbn', 'published_date'],
            extra_siblings=1,
            max_siblings=10,
        ),
    )

StackedInline → Collection with layout

Django Admin:

class AddressInline(admin.StackedInline):
    model = Address
    fields = ['street', ('city', 'state', 'zip')]

class CustomerAdmin(admin.ModelAdmin):
    inlines = [AddressInline]

Layout API ⚠️ Requires djadmin-formset plugin:

class CustomerAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
        Collection('addresses',
            model=Address,
            layout=Layout(
                Field('street'),
                Row(
                    Field('city', css_classes=['flex-1']),
                    Field('state', css_classes=['flex-1']),
                    Field('zip', css_classes=['flex-1']),
                ),
            ),
        ),
    )

Nested Inlines → Nested Collections

Django Admin: Not directly supported

Layout API ⚠️ Requires djadmin-formset plugin:

class CompanyAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Collection('departments',
            model=Department,
            layout=Layout(
                Field('name'),
                Collection('employees',  # Nested!
                    model=Employee,
                    fields=['name', 'title', 'email'],
                ),
            ),
        ),
    )


Common Patterns

Pattern 1: Simple Form

Before:

class ArticleAdmin(admin.ModelAdmin):
    fields = ['title', 'content', 'author', 'published']

After:

class ArticleAdmin(ModelAdmin):
    layout = Layout(
        Field('title'),
        Field('content', widget='textarea'),
        Field('author'),
        Field('published'),
    )

Pattern 2: Grouped Fields

Before:

class ProductAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Product Info', {
            'fields': ('name', 'sku', ('price', 'cost')),
        }),
        ('Inventory', {
            'fields': ('stock', 'reorder_level'),
        }),
    )

After (automatic conversion works, or manual):

class ProductAdmin(ModelAdmin):
    layout = Layout(
        Fieldset('Product Info',
            Field('name'),
            Field('sku'),
            Row(
                Field('price', css_classes=['flex-1', 'pr-2']),
                Field('cost', css_classes=['flex-1', 'pl-2']),
            ),
        ),
        Fieldset('Inventory',
            Field('stock'),
            Field('reorder_level'),
        ),
    )

Pattern 3: Collapsible Sections

Before:

class SettingsAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Basic', {
            'fields': ('site_name', 'tagline'),
        }),
        ('Advanced', {
            'classes': ('collapse',),
            'fields': ('debug', 'cache_timeout'),
        }),
    )

After:

class SettingsAdmin(ModelAdmin):
    layout = Layout(
        Fieldset('Basic',
            Field('site_name'),
            Field('tagline'),
        ),
        Fieldset('Advanced',
            Field('debug'),
            Field('cache_timeout'),
            css_classes=['collapse'],
        ),
    )

Pattern 4: Wide Fields

Before:

class PageAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {
            'fields': ('title', 'slug'),
        }),
        ('Content', {
            'fields': ('body',),
            'classes': ('wide',),
        }),
    )

After:

class PageAdmin(ModelAdmin):
    layout = Layout(
        Field('title'),
        Field('slug'),
        Fieldset('Content',
            Field('body', widget='textarea', attrs={'rows': 20}),
            css_classes=['wide'],
        ),
    )


Troubleshooting

Error: "Cannot specify both 'fieldsets' and 'layout'"

Cause: You have both fieldsets and layout defined.

Solution: Remove fieldsets, the metaclass will auto-convert it.

# ❌ BAD
class MyAdmin(ModelAdmin):
    fieldsets = (...)
    layout = Layout(...)  # Conflict!

# ✅ GOOD - Pick one
class MyAdmin(ModelAdmin):
    fieldsets = (...)  # Auto-converts to layout

# Or:
class MyAdmin(ModelAdmin):
    layout = Layout(...)

Nested Tuples Not Converting Correctly

Issue: Complex nested tuples may not convert perfectly.

Solution: Use manual Layout for complex cases.

# Django admin with nested tuples
fieldsets = (
    ('Complex', {
        'fields': (('a', 'b'), ('c', ('d', 'e'))),
    }),
)

# Better: Manual Layout
layout = Layout(
    Fieldset('Complex',
        Row(
            Field('a', css_classes=['flex-1']),
            Field('b', css_classes=['flex-1']),
        ),
        Row(
            Field('c', css_classes=['flex-1']),
            Row(
                Field('d', css_classes=['flex-1']),
                Field('e', css_classes=['flex-1']),
            ),
        ),
    ),
)

Inlines Not Working

Issue: Collections require plugin, error at startup.

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]

Solution: Install the plugin:

pip install django-admin-deux[djadmin-formset]

Readonly Fields

Issue: Layout API doesn't have direct readonly_fields support.

Solution: Use widget or create custom field:

# Option 1: Use TextInput with readonly attribute
Field('created_at',
    widget='text',
    attrs={'readonly': True}
)

# Option 2: Use HiddenInput if not displayed
Field('created_at', widget='hidden')

# Option 3: Custom display in template (advanced)

Custom Widgets

Django Admin:

class MyAdmin(admin.ModelAdmin):
    formfield_overrides = {
        models.TextField: {'widget': Textarea(attrs={'rows': 4})},
    }

Layout API:

class MyAdmin(ModelAdmin):
    layout = Layout(
        Field('description', widget='textarea', attrs={'rows': 4}),
    )


Migration Checklist

When migrating a Django admin to djadmin:

  • Replace admin.ModelAdmin with ModelAdmin
  • Replace admin.site.register with @register decorator
  • Decide: Keep fieldsets (auto-convert) or convert to layout manually
  • Convert inlines to Collection components
  • Install djadmin-formset plugin if using Collections
  • Test create and update forms
  • Verify field ordering and layout
  • Check validation and error messages
  • Test with real data
  • Update any custom templates if needed

Next Steps


Summary

Easy Migration Path: 1. Copy existing fieldsets → Works automatically 2. Or convert to layout manually for more control 3. Add Collection for inlines (requires plugin) 4. Test and iterate

Key Differences: - Layout API is more explicit (Field, Fieldset, Row) - Better type safety and validation - More flexible (horizontal layouts, nested collections) - Progressive enhancement (plugin for advanced features)

The Layout API is designed to make migration from Django admin as smooth as possible while providing significantly more flexibility and features.