Using one form to edit two models

Note: This post is a near-direct translation of an original post by Antonio Melé on the Django.es blog.

Sometimes we need to change information which relates to two or more different models from within only one HTML form. It is quite common, for example, to define a user profile within an app wherein you need to be able to edit information pertaining to both the Django authentication system's User model and your own user profile model simultaneously. We're going to see how this can be done. To follow this example, the django.contrib.auth app needs to be present in the list of installed applications in your project (the INSTALLED_APPS setting).

Suppose we have the following Profile model defined in our models.py:

from django.contrib.auth.models import Userfrom django.db import modelsclass Profile(models.Model):    user = models.OneToOneField(User, related_name='profile')    telephone_number = models.CharField(max_length=16)    address = models.TextField()

We then create a ModelForm subclass for this Profile model and another for django.contrib.auth's built-in User model. Our forms.py file should look like this:

from django import formsfrom django.contrib.auth.models import Userfrom models import Profileclass UserForm(forms.ModelForm):    class Meta(object):        model = User        fields = ('username', 'first_name', 'last_name', 'email')class ProfileForm(forms.ModelForm):    class Meta(object):        model = Profile

As you can see, we only want to edit the fields username, first_name, last_name and email on the User model.

Now we can create the view which will serve the form for editing a user's profile. In order to do this we need to instantiate the two ModelForm subclasses we defined previously. Our views.py should contain the following code:

from django.contrib.auth.decorators import login_requiredfrom django.shortcuts import render_to_responsefrom django.template import RequestContext, loaderfrom forms import ProfileForm, UserForm@login_requireddef edit_profile(request):    if request.method == 'POST':        # The forms submitted by the client.        user_form = UserForm(request.POST, instance=request.user)        profile_form = ProfileForm(request.POST, instance=request.user.profile)        if user_form.is_valid() and profile_form.is_valid():            # The forms validated correctly.            user_form.save()            profile_form.save()            # Change this location as necessary.            return HttpResponseRedirect('/form-saved-successfully/')    else:        # Initialise the forms.        user_form = UserForm(instance=request.user)        profile_form = ProfileForm(instance=request.user.profile)    return render_to_response('edit_profile.html',        {'user_form': user_form, 'profile_form': profile_form},        context_instance=RequestContext(request))

Here we used the @login_required decorator, as clients should be authenticated in order to edit their profile. If the request arrives with the POST method it means they have submitted the forms, in which case we create two Form instances - one for the User model and another for the Profile - with the received data. We validate both forms as we would with only one form, before saving both too. If they are both valid the client is redirected to a 'success' URL. If the forms are invalid, or the request has arrived via the GET HTTP method, we render a template called edit_profile.html to which we pass the two forms. If the forms were submitted but invalid, they will contain the generated errors and data with which they were submitted; otherwise we create two new Form instances and render the template with them. The forms, as they appear in our HTML template, look something like this:

<form action="" method="POST">  {{ user_form }}  {{ profile_form }}  <input type="submit" value="Save changes" /></form>

This template renders a HTML form which contains the specified fields from the User model followed by those from the associated Profile, and which will correctly save any change made in either of the two models.