Introduction
Django's admin uses auto-generated forms by default, but you can provide custom ModelForm subclasses for advanced validation, custom widgets, and extra fields. This lesson covers form customization from simple widget changes to dynamic form generation.
Key Concepts
form attribute: Set on ModelAdmin to specify a custom ModelForm class.
get_form(): Method to dynamically modify the form class based on request or object.
clean(): Form method for cross-field validation.
save_model(): ModelAdmin method for post-save logic with form data.
Media class: Inner class on forms to include custom CSS and JavaScript.
Real World Context
An event management admin needs a form where selecting 'recurring' as the event type dynamically shows recurrence fields (frequency, end date), and publishing an event requires a venue to be set. A custom AdminForm with clean() validation and JavaScript-driven conditional fields makes the admin smart enough to guide organizers through the correct workflow.
Deep Dive
Customize the forms used in the admin change view for validation and advanced functionality.
Basic Form Customization
pythonfrom django import forms from django.contrib import admin from .models import Article class ArticleAdminForm(forms.ModelForm): class Meta: model = Article fields = '__all__' widgets = { 'body': forms.Textarea(attrs={ 'rows': 20, 'cols': 80, 'class': 'vLargeTextField', }), 'summary': forms.Textarea(attrs={'rows': 3}), } def clean_title(self): title = self.cleaned_data['title'] if len(title) < 10: raise forms.ValidationError('Title must be at least 10 characters.') return title def clean(self): cleaned_data = super().clean() status = cleaned_data.get('status') pub_date = cleaned_data.get('pub_date') if status == 'published' and not pub_date: raise forms.ValidationError( 'Published articles must have a publication date.' ) return cleaned_data @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): form = ArticleAdminForm
Dynamic Form Based on Request
python@admin.register(Article) class ArticleAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) # Add help text dynamically form.base_fields['title'].help_text = 'Enter a compelling title' # Modify queryset for foreign keys if not request.user.is_superuser: form.base_fields['author'].queryset = User.objects.filter( groups__name='Authors' ) return form def get_readonly_fields(self, request, obj=None): if obj and obj.status == 'published': # Can't edit published articles return ['title', 'slug', 'body', 'author'] return []
Custom Widgets
pythonfrom django.contrib.admin.widgets import AdminDateWidget, FilteredSelectMultiple from django.forms.widgets import Select class ArticleAdminForm(forms.ModelForm): class Meta: model = Article fields = '__all__' widgets = { 'pub_date': AdminDateWidget(), 'tags': FilteredSelectMultiple( verbose_name='Tags', is_stacked=False, ), 'category': Select(attrs={'class': 'select2'}), }
Rich Text Editor Integration
python# Using django-ckeditor or similar from ckeditor.widgets import CKEditorWidget class ArticleAdminForm(forms.ModelForm): body = forms.CharField(widget=CKEditorWidget()) class Meta: model = Article fields = '__all__'
Conditional Fields
pythonclass ArticleAdminForm(forms.ModelForm): schedule_publish = forms.BooleanField( required=False, help_text='Schedule this article for future publication' ) class Meta: model = Article fields = '__all__' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Pre-check if article has future pub_date if self.instance.pk: from django.utils import timezone if self.instance.pub_date and self.instance.pub_date > timezone.now(): self.fields['schedule_publish'].initial = True class Media: js = ('admin/js/article_form.js',) # JS to show/hide fields
Form with Extra Fields
pythonclass ArticleAdminForm(forms.ModelForm): # Extra field not in model send_notification = forms.BooleanField( required=False, label='Send notification', help_text='Notify subscribers about this article' ) class Meta: model = Article fields = '__all__' @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): form = ArticleAdminForm def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) if form.cleaned_data.get('send_notification'): send_article_notification(obj)
Common Pitfalls
-
Setting form and fields on the same ModelAdmin: When you set form = MyCustomForm, the Meta.fields on the form class takes precedence. Setting fields on ModelAdmin as well can cause confusion about which controls the visible fields.
-
Not calling super().clean() in form validation: Skipping super().clean() bypasses Django's built-in validation including unique checks and required field validation.
-
Using form.save() instead of save_model(): In the admin context, save_model() is the correct hook for post-save logic because it receives the request and form. Overriding form.save() misses the request context.
Best Practices
-
Use get_form() for dynamic changes: Instead of creating multiple form classes, use get_form() to modify a single form class based on user permissions or object state.
-
Add extra non-model fields for workflow triggers: Fields like 'send_notification' or 'approve_and_publish' that are not database columns can be added to the form and processed in save_model().
-
Use widgets from django.contrib.admin: Django provides AdminDateWidget, FilteredSelectMultiple, and other admin-specific widgets that integrate with the admin's JavaScript and CSS.
Summary
- Set form = MyCustomForm on ModelAdmin for custom validation and widgets.
- Override get_form() for dynamic form modification based on request or object.
- Use clean() for cross-field validation and clean_<field>() for single-field rules.
- Process extra form fields in save_model() for workflow triggers.
- Use Django's admin widgets (AdminDateWidget, FilteredSelectMultiple) for consistent styling.
Code Examples
class ArticleAdminForm(forms.ModelForm):
class Meta:
model = Article
fields = '__all__'
widgets = {
'body': forms.Textarea(attrs={'rows': 20, 'class': 'vLargeTextField'}),
'tags': FilteredSelectMultiple('Tags', is_stacked=False),
}
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
form = ArticleAdminForm