Extending ModelAdmin Views with Arbitrary Forms
Ever needed to set up a django admin form with extra validation? For example, you might be creating a form to create multiple objects at once. Perhaps your Client model needs to create a User during the Add view.
Perhaps your Affiliate model needs to generate a Client model and User in the Add view, and allow the user to edit those same models from one Change view.
All of the above can be accomplished with a custom admin form.
Overriding the admin form
ModelAdmins can specify a “form” attribute that overrides the default ModelForm used by the admin add/change views.
We can use this override to set up a ModelForm that intelligently validates the extra non model fields and also saves the related models correctly.
Adding an extra field
class MyForm(forms.ModelForm):
extra_field = forms.CharField()
class Meta:
model = MyModel
class MyAdmin(admin.ModelAdmin):
form = MyForm
Our admin form now contains an extra required field – “extra_field”.
Adding and editing a related model
To add or edit a related model, we need to modify the form to do a useful __init__ and a useful save().
In our __init__ function we need to fill the initial dict with the existing model fields. In our save function we need to write the data to our related model.
You will need to adapt the code to your specific scenario, as a related model can be “related” in any number of ways. A simple foreign key? A reverse relation? It could even be a relation not specified in the model.
class MyForm(forms.ModelForm):
related_model_field1 = forms.CharField()
related_model_field2 = forms.CharField()
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if self.instance.id:
related_model = get_related_model() # pull related model somehow
self.initial.update({
'related_model_field1': related_model.field1,
'related_model_field2': related_model.field2,
})
def save(self, *args, **kwargs):
if self.instance.id:
related_model = self.instance.related_model
else:
related_model = RelatedModel()
related_model.related_model_field1 = self.cleaned_data.get('related_model_field1')
related_model.related_model_field2 = self.cleaned_data.get('related_model_field2')
related_model.save()
self.instance.related_model = related_model # adapt to your relation
super(MyForm, self).save(*args, **kwargs)
class MyAdmin(admin.ModelAdmin):
form = MyForm
We’re only doing a few things: validating the new fields in your ModelForm, automatically creating the related model OR updating it if it already exists, and finally populating the initial data if the model is being edited.
Shorten the code – use list comprehensions
As we use more and more fields, explicitly specifying each field and value in the __init__ and save function takes more and more lines of code.
I tend to use a dictionary mapping local form field names to the related object field names so that I can simply use a few list comprehensions to handle the __init__ and save() code.
class MyForm(forms.ModelForm):
related_model_field1 = forms.CharField()
related_model_field2 = forms.CharField()
RELATED_FIELD_MAP = {
# map local field names to the object field names.
'related_model_field1': 'field1',
'related_model_field2': 'field2',
}
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if self.instance.id:
related_model = get_related_model() # pull related model somehow
self.initial.update(
dict([(field, getattr(related_model, target_field) for field, target_field in self.RELATED_FIELD_MAP.iteritems())]))
def save(self, *args, **kwargs):
related_model = self.instance.related_model if self.instance.id else RelatedModel()
[setattr(related_model, target_field, self.cleaned_data.get(field) for
field, target_field in self.RELATED_FIELD_MAP.iteritems()]
related_model.save()
self.instance.related_model = related_model # adapt to your relation..
super(MyForm, self).save(*args, **kwargs)
class MyAdmin(admin.ModelAdmin):
form = MyForm
You can be even more DRY by setting up your RELATED_FIELD_MAP to contain “target_field, forms.CharField()” values and instantiate the form manually with the fields.
Using the concepts from this post, you can use the django admin application to generate much more complex, multi-object edit/add forms that appear as one form.
It’s supremely useful when you still want to use the admin interface for 90% of what you do.