Tech Blog

Django Multiple Page Forms

Django Multiple Page Forms

Recently updated on

I recently started working on another new project here at Imaginary Landscape, and this one looked rather enticing as it threw some stuff my way that I haven't had a chance to play with much recently. First on that chopping block was a multi-page registration form application. Immediately I remembered reading about Django's Form Wizard module and thought it'd be a great way to handle this small application. At least I thought so until I finished reading the specifications for the project and realized that the user must be able to move both backwards and forwards through the form. Oh, and as a bonus, I was dealing with sensitive information, so I had to be careful about what I stashed in sessions.

So unfortunately Form Wizard wasn't going to be able to handle my needs, as it doesn't really support going backwards in the forms. Also, it's built for use with django.forms.Form, not django.forms.ModelForm like I wanted to use since I have to store all this information in a database anyway. In the end I went with storing object IDs in the user's session. This would allow me to grab the data that was submitted based off the ID, avoid storing sensitive information in sessions, and provide me with all the functionality that I was looking to achieve. This actually ended up being more straightforward than I had initially expected. First you need some forms:

class UserInfoForm(forms.ModelForm):
    class Meta:
        model = models.UserInfo
               
class AddressForm(forms.ModelForm):
    class Meta:
        model = models.Address

Let's just pretend these are connected to very basic models that fit their namesake. Now we'll need to create some views:

def user_information(request):

    if request.method == "POST":
        form = forms.UserInfoForm(request.POST)
        if form.is_valid():
            user_info = form.save()
            request.session['user_info_id'] = user_info.id
            return HttpResponseRedirect(
                reverse("address_form"))
    else:
        if 'user_info_id' in request.session:
            try:
                user_info_obj = models.UserInfo.objects.get(
                    id=request.session['user_info_id']
                )
                form = forms.UserInfoForm(instance=user_info_obj)
            except ObjectDoesNotExist:
                del request.session['user_info_id']
                form = forms.UserInfoForm()
        else:
            form = forms.UserInfoForm()
    return render_to_response(
        "app/user_info.html",
        locals(), context_instance=RequestContext(request))

As you can see this is fairly straightforward. It's just how you'd handle a normal form, but after we save our form we're injecting the "user_info.id" into the session. We'll be needing this if we decide to go backwards from our next form. If we're not posting, we check to see if that ID is in the session. If it is, we load that object, and initialize the form it. If that object doesn't exist, we clear the session item and create a blank form for the user to fill out. Now we have our second form:

        
def address(request):

    if 'user_info_id' not in request.session:
        return HttpResponseRedirect(
            reverse("user_info_form"))
    if request.method == "POST":
        user_info_obj = models.UserInfo.objects.get(
            id=request.session['user_info_id']
        )
        new_address = models.Address(patient=user_info_obj)
        form = forms.AddressForm(request.POST,
            instance=new_address)
        if form.is_valid():
            address_info = form.save()
            request.session['address_info_id'] = address_info.id
            ## Send them to a thank you page
    else:
        if 'address_info_id' in request.session:
            try:
                user_info_obj = models.UserInfo.objects.get(
                    id=request.session['user_info_id']
                )
            except ObjectDoesNotExist:
                return HttpResponseRedirect(
                    reverse("user_info_form"))
            try:
                address_obj = models.Address.objects.get(
                    id=request.session['address_info_id']
                )
                form = forms.AddressForm(instance=address_obj)
            except ObjectDoesNotExist:
                user_info_obj = models.UserInfo.objects.get(
                    id=request.session['user_info_id']
                )
                form = forms.AddressForm(
                    initial={'user': user_info_obj.id})
        else:
            user_info_obj = models.UserInfo.objects.get(
                id=request.session['user_info_id']
            )
            form = forms.AddressForm(
                initial={'user': user_info_obj.id})
    return render_to_response(
        "app/address.html",
        locals(), context_instance=RequestContext(request))

This follows the same basic principle as the first form. As an added treat, it also uses some data from the first form. If we don't have that data, that means the user didn't fill out the first form. In that case, we send them back to the first form to properly fill it out. We also stash the ID of the address object in the sessions as well, which if this is the last form (which it is in this example) isn't totally necessary. You can choose to clear all the items from the session that you put in there if you prefer. However, if the user uses their browser's back button to go back to the first form and submit it, then the second form will still be initialized with the data they previously put in there.

Surprisingly, that pretty much sums up creating a multi-page form in Django. Naturally there are many ways to accomplish a multi-page form. If you're using simple forms and don't need to enable users to browse backwards, then I highly recommend FormWizard for ease of use. It's really easy to use, and handles most use cases. If you need something a bit more complex, the code above should work out for you. Though I must warn you, this code was adapted to this blog post from an existing project. As such, there might be some minor issues I missed in adaptation. Please let me know in the comments if you see any such issues.

Comments

Comments are closed.