Updating two models in a single post

Published:

This comes up a lot in #django ... and the solution is [as many are with Django] much simpler than people assume.

Frequently I see people reach for formsets, in the mistaken conclusion that formsets are for operating on related models. The sad part is that formsets rely on the same functionality that the proper solution relies on, but the seeker does not see it.

Somewhere people get the idea that for a single <form> submission, they can only use a single Form class... but the formset should show that that's not true; it uses multiple forms from the one submission.

The answer

The ModelForms

Firstly, the forms don't need to be anything special, but for one point:

class UserForm(models.ModelForm):
    model = models.User
    fields = [...]

class ProfileForm(models.ModelForm):
    model = models.Profile
    fields = [...]
    exclude = ['user']

We omit the user field so the ProfileForm won't show a drop-down list of Users to pick from.

The view

So, as a simple example, here's a view that updates a User and their Profile in one view:

def user_edit(request):

    if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)

        if all([user_form.is_valid(), profile_form.is_valid()]):
            user = user_form.save()
            profile = profile_form.save()
            return redirect(user)

    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)

    return render(request, 'accounts/user_form.html', {
        'user_form': user_form,
        'profile_form': profile_form,
    })

The two most significant things to notice here:

  1. the use of all() instead of user_form.is_valid() and profile_form.is_valid()

    This is because when using and Python will "shortcut" the if clause if the first form is not valid, and so not run validation on the second form.

    May not sound like much, but imagine how tedious it will be for your user when, after they finally get all the fields for the first form right, a whole new slew of errors show up?

  2. Two forms in the context.

    Just render them all inside the one <form> tag, and you're right.

    If any of the field names clash, pass a prefix= when constructing one of the forms, to prefix its field names so they're unique.

Creating two related records

So the above works great if you have existing instances, but what if you want to create two related models?

Fortunately, the answer turns out to be almost as simple:

def user_create(request):

    if request.method == 'POST':
        user_form = UserForm(request.POST)
        profile_form = ProfileForm(request.POST)

        if all([user_form.is_valid(), profile_form.is_valid()]):
            user = user_form.save()
            profile = profile_form.save(commit=False)
            profile.user = user
            profile.save()
            return redirect(user)

    else:
        user_form = UserForm()
        profile_form = ProfileForm()

    return render(request, 'accounts/user_form.html', {
        'user_form': user_form,
        'profile_form': profile_form,
    })

The difference here is we need to edit the Profile instance before it's saved, to set the user field to our newly created User instance.

See here for more details on .save(commit=False).