Django — Extension of ModelAdmin Admin Views: Arbitrary Form Validation with AdminForm

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.

9 thoughts on “Django — Extension of ModelAdmin Admin Views: Arbitrary Form Validation with AdminForm

  1. I googled “django admin form” and this is on the first page.
    I was looking for exactly what you described – easy way to create one model and all related models as well.
    And you post is kinda useful. πŸ˜‰

    1. Ahhh!!! I have a new method for doing this which involves overriding the form used by the ModelAdmin.

      I will update the post. I definitely recommend against hacking into Django core.

  2. Hi, I was almost giving up and found this, it was exactly what I needed so many thanks πŸ™‚

    Now, what if I wanted to make the fields readonly in the admin, since its not part of the model it gives a very colorful error, is it possible to make those fields readonly?

    1. You could specify a widget with readonly= true.

      readonly_field = forms.CharField(widget=forms.TextInput(attrs={‘readonly’: ‘true’}))

      1. Oh jeez is like my brain cells are fried πŸ˜› I have been dead set in including the fields in the readonly_fields list in the admin that I hadn’t thought of that and it’s so simple. Thanks mate πŸ™‚

    2. Hey no prob! It’s not exactly like the django admin readonly fields (those don’t use input boxes) so if you wanted you could make your own ReadOnlyWidget that doesn’t use an input box but only outputs text. I believe there are some on djangosnippets.org

      I just remembered though: be careful when using html readonly as it doesn’t mean the user can’t POST whatever he/she wants using dev tools. Shouldn’t be a big deal if we’re talking about this blog post since the related models have to be explicitly saved / fields mapped.

      Good luck!

  3. Any way to define a new method in a parent ModelForm; such that it cane accessed in child forms that inherit from it?

    1. Hey Derek,

      Why not? That’s how inheritance works! This form idea is kinda crazy but works in some specific scenarios like user creation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s