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?
- Automatic Conversion
- Manual Conversion
- Inlines Migration
- Common Patterns
- Troubleshooting
Why Migrate?¶
Limitations of Django Admin¶
- Rigid Syntax - Tuples are hard to read and maintain
- No Type Safety - Easy to make mistakes
- Limited Layouts - Only vertical stacking
- No Conditionals - Can't show/hide fields dynamically
- Inline Limitations - Limited control over inline appearance
Advantages of Layout API¶
- ✅ Declarative - Clean, readable Python syntax
- ✅ Type-safe - Dataclasses with validation
- ✅ Flexible - Fieldsets, rows, and nested layouts
- ✅ Progressive - Start simple, add features as needed
- ✅ Extensible - Plugin system for advanced features
- ✅ 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:
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:
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:
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.ModelAdminwithModelAdmin - Replace
admin.site.registerwith@registerdecorator - Decide: Keep
fieldsets(auto-convert) or convert tolayoutmanually - Convert
inlinestoCollectioncomponents - Install
djadmin-formsetplugin 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¶
- Component Reference - Detailed API for each component
- Examples - Real-world usage patterns
- Layout API Overview - Complete feature overview
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.