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
User
s 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:
-
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 firstform
is not valid, and so not run validation on the secondform
.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?
-
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)
.