MultiForm and MultiModelForm

A container that allows you to treat multiple forms as one form. This is great for using more than one form on a page that share the same submit button. MultiForm imitates the Form API so that it is invisible to anybody else (for example, generic views) that you are using a MultiForm.

There are a couple of differences, though. One lies in how you initialize the form. See this example:

class UserProfileMultiForm(MultiForm):
    form_classes = {
        'user': UserForm,
        'profile': ProfileForm,
    }

UserProfileMultiForm(initial={
    'user': {
        # User's initial data
    },
    'profile': {
        # Profile's initial data
    },
})

The initial data argument has to be a nested dictionary so that we can associate the right initial data with the right form class.

The other major difference is that there is no direct field access because this could lead to namespace clashes. You have to access the fields from their forms. All forms are available using the key provided in form_classes:

form = UserProfileMultiForm()
# get the Field object
form['user'].fields['name']
# get the BoundField object
form['user']['name']

MultiForm, however, does all you to iterate over all the fields of all the forms.

{% for field in form %}
  {{ field }}
{% endfor %}

If you are relying on the fields to come out in a consistent order, you should use an OrderedDict to define the form_classes.

from collections import OrderedDict

class UserProfileMultiForm(MultiForm):
    form_classes = OrderedDict((
        ('user', UserForm),
        ('profile', ProfileForm),
    ))

Working with ModelForms

MultiModelForm adds ModelForm support on top of MultiForm. That simply means that it includes support for the instance parameter in initialization and adds a save method.

class UserProfileMultiForm(MultiModelForm):
    form_classes = {
        'user': UserForm,
        'profile': ProfileForm,
    }

user = User.objects.get(pk=123)
UserProfileMultiForm(instance={
    'user': user,
    'profile': user.profile,
})

Working with CreateView

It is pretty easy to use MultiModelForms with Django’s CreateView, usually you will have to override the form_valid() method to do some specific saving functionality. For example, you could have a signup form that created a user and a user profile object all in one:

# forms.py
from django import forms
from authtools.forms import UserCreationForm
from betterforms.multiform import MultiModelForm
from .models import UserProfile

class UserProfileForm(forms.ModelForm):
    class Meta:
        fields = ('favorite_color',)

class UserCreationMultiForm(MultiModelForm):
    form_classes = {
        'user': UserCreationForm,
        'profile': UserProfileForm,
    }

# views.py
from django.views.generic import CreateView
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from .forms import UserCreationMultiForm

class UserSignupView(CreateView):
    form_class = UserCreationMultiForm
    success_url = reverse_lazy('home')

    def form_valid(self, form):
        # Save the user first, because the profile needs a user before it
        # can be saved.
        user = form['user'].save()
        profile = form['profile'].save(commit=False)
        profile.user = user
        profile.save()
        return redirect(self.get_success_url())

Note

In this example, we used the UserCreationForm from the django-authtools package just for the purposes of brevity. You could of course use any ModelForm that you wanted to.

Of course, we could put the save logic in the UserCreationMultiForm itself by overriding the MultiModelForm.save() method.

class UserCreationMultiForm(MultiModelForm):
    form_classes = {
        'user': UserCreationForm,
        'profile': UserProfileForm,
    }

    def save(self, commit=True):
        objects = super(UserCreationMultiForm, self).save(commit=False)

        if commit:
            user = objects['user']
            user.save()
            profile = objects['profile']
            profile.user = user
            profile.save()

        return objects

If we do that, we can simplify our view to this:

class UserSignupView(CreateView):
    form_class = UserCreationMultiForm
    success_url = reverse_lazy('home')

Working with UpdateView

Working with UpdateView likewise is quite easy, but you most likely will have to override the django.views.generic.edit.FormMixin.get_form_kwargs method in order to pass in the instances that you want to work on. If we keep with the user/profile example, it would look something like this:

# forms.py
from django import forms
from django.contrib.auth import get_user_model
from betterforms.multiform import MultiModelForm
from .models import UserProfile

User = get_user_model()

class UserEditForm(forms.ModelForm):
    class Meta:
        fields = ('email',)

class UserProfileForm(forms.ModelForm):
    class Meta:
        fields = ('favorite_color',)

class UserEditMultiForm(MultiModelForm):
    form_classes = {
        'user': UserEditForm,
        'profile': UserProfileForm,
    }

# views.py
from django.views.generic import UpdateView
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from django.contrib.auth import get_user_model
from .forms import UserEditMultiForm

User = get_user_model()

class UserSignupView(UpdateView):
    model = User
    form_class = UserEditMultiForm
    success_url = reverse_lazy('home')

    def get_form_kwargs(self):
        kwargs = super(UserSignupView, self).get_form_kwargs()
        kwargs.update(instance={
            'user': self.object,
            'profile': self.object.profile,
        })
        return kwargs

API Reference

class betterforms.multiform.MultiForm[source]

The main interface for customizing MultiForms is through overriding the form_classes class attribute.

Once a MultiForm is instantiated, you can access the child form instances with their names like this:

>>> class MyMultiForm(MultiForm):
        form_classes = {
            'foo': FooForm,
            'bar': BarForm,
        }
>>> forms = MyMultiForm()
>>> foo_form = forms['foo']

You may also iterate over a multiform to get all of the fields for each child instance.

MultiForm API

The following attributes and methods are made available for customizing the instantiation of multiforms.

__init__(*args, **kwargs)[source]

The __init__() is basically just a pass-through to the children form classes’ initialization methods, the only thing that it does is provide special handling for the initial parameter. Instead of being a dictionary of initial values, initial is now a dictionary of form name, initial data pairs.

UserProfileMultiForm(initial={
    'user': {
        # User's initial data
    },
    'profile': {
        # Profile's initial data
    },
})
form_classes

This is a dictionary of form name, form class pairs. If the order of the forms is important (for example for output), you can use an OrderedDict instead of a plain dictionary.

get_form_args_kwargs(key, args, kwargs)[source]

This method is available for customizing the instantiation of each form instance. It should return a two-tuple of args and kwargs that will get passed to the child form class that corresponds with the key that is passed in. The default implementation just adds a prefix to each class to prevent field value clashes.

Form API

The following attributes and methods are made available for mimicking the Form API.

media
is_bound
cleaned_data

Returns an OrderedDict of the cleaned_data for each of the child forms.

is_valid()[source]
non_field_errors()[source]
as_table()[source]
as_ul()[source]
as_p()[source]
is_multipart()[source]
hidden_fields()[source]
visible_fields()[source]
class betterforms.multiform.MultiModelForm[source]

MultiModelForm differs from MultiForm only in that adds special handling for the instance parameter for initialization and has a save() method.

__init__(*args, **kwargs)[source]

MultiModelForm's initialization method provides special handling for the instance parameter. Instead of being one object, the instance parameter is expected to be a dictionary of form name, instance object pairs.

UserProfileMultiForm(instance={
    'user': user,
    'profile': user.profile,
})
save(commit=True)[source]

The save() method will iterate through the child classes and call save on each of them. It returns an OrderedDict of form name, object pairs, where the object is what is returned by the save method of the child form class. Like the ModelForm.save method, if commit is False, MultiModelForm.save() will add a save_m2m method to the MultiModelForm instance to aid in saving the many-to-many relations later.

Addendum About django-multiform

There is another Django app that provides a similar wrapper called django-multiform that provides essentially the same features as betterform’s MultiForm. I searched for an app that did this feature when I started work on betterform’s version, but couldn’t find one. I have looked at django-multiform now and I think that while they are pretty similar, but there are some differences which I think should be noted:

  1. django-multiform’s MultiForm class actually inherits from Django’s Form class. I don’t think it is very clear if this is a benefit or a disadvantage, but to me it seems that it means that there is Form API that exposed by django-multiform’s MultiForm that doesn’t actually delegate to the child classes.
  2. I think that django-multiform’s method of dispatching the different values for instance and initial to the child classes is more complicated that it needs to be. Instead of just accepting a dictionary like betterform’s MultiForm does, with django-multiform, you have to write a dispatch_init_initial method.