Skip to Content

Technology Blog

Technology Blog

Multi-page Forms in Django

Recently updated on

This blog post started as a record of my experience with creating multipage forms in Django, and an exploration of some of the techniques I used.  Since that time, I have developed a separate multipage form app that implements and expands on the features discussed in the original article.  This updated version of the original post will explore this app and its use in the context of an example project.

Introduction

Most online forms fit on a single page.  Think of a "join our forum" or "contact us" form into which the user enters a name, email address, and maybe a few other pieces of information.  If you're building this kind of functionality into a Django site, you will probably want to take advantage of Django's built-in form classes.  These are especially handy when dealing with model forms, where the form fields correspond to the fields on a model that will be saved in your database.

But what if you need a form that spans more than one page?  Like a multipage job application where your personal details are on page 1, your education is on page 2, and so on?  We have an app to help you to add such a form to your website.  You can download it here:

https://github.com/ImaginaryLandscape/django-multipage-form

The app’s README should explain everything needed to use the app in your own project, but if you’re interested in a walkthrough, we have also developed an example project that demonstrates all the functions of the multipage form app.  That project can be downloaded here:

https://github.com/ImaginaryLandscape/multipage-form-demo

Feel free to clone the repo, get the demo up and running, and use or modify it to your own purposes.  Or just follow along here in the post.  We’ll go step-by-step through the creation of the same multipage job application form used in the example project.
Now let's jump in!

Requirements

  • Python 3
  • Django 3.2
  • A basic knowledge of how Django works

The Model

We'll be working with a model form, so the first thing we need is a model that will represent a submitted job application.  The following snippet is from the example project:

job_application/models.py

from django.db import models
from multipage_form.models import MultipageModel

class JobApplication(MultipageModel):
    # stage 1 fields
    first_name = models.CharField(max_length=20, blank=True)
    middle_name = models.CharField(max_length=20, blank=True)
    last_name = models.CharField(max_length=20, blank=True)
    # stage 2 fields
    education_level = models.CharField(max_length=20, blank=True)
    year_graduated = models.CharField(max_length=4, blank=True)
    # stage 3 fields
    prior_company = models.CharField(max_length=20, blank=True)
    prior_experience = models.TextField(blank=True)
    anything_more = models.BooleanField(default=False)
    # stage 3b fields                                                                                                                                                                  
    personal_statement = models.TextField(blank=True)
    # stage 4 fields
    all_is_accurate = models.BooleanField(default=False)

The attributes defined on this model are simply the fields that correspond to user-submitted values.  Note that there is no real difference on the model between a “stage 1” field and a “stage 2” field.  These groupings are indicated in comments merely to give an idea of the example form’s structure. (The “stage 3b” designation will be addressed below.)

Note also that an argument of “blank=True” has been passed to all of the text-based fields.  With the multipage form, it's important to declare your model fields with “blank=True” for string values or “null=True” / “default=False” for other values. This is because unlike with a typical, single-page form, the values on the model must be saved one chunk at a time, as the user progresses through the different form pages.

This is true even for fields for which the user should be required to provide a response. Therefore, if you want a given field to be required, it should be done by making the field required at the form level instead of at the database level.  We’ll talk more about that below.

The Form

This is where the real action is. Let’s start with a simplified version of the “forms.py” module from the example project:

job_application/forms.py

from multipage_form.forms import MultipageForm, ChildForm
from .models import JobApplication

class JobApplicationForm(MultipageForm):
    model = JobApplication
    starting_form = "Stage1Form"

    class Stage1Form(ChildForm):
        next_form_class = "Stage2Form"

        class Meta:
            fields = ["first_name", "middle_name", "last_name"]

    class Stage2Form(ChildForm):

        class Meta:
            fields = ["education_level", "year_graduated"]
            help_text = {
                "education_level": "Indicate the highest level of education you have attained"
            }

Notice that our form inherits from the “MultipageForm” class, and that it contains multiple nested classes that inherit from the “ChildForm” class.   The outer class specifies the model that we’ll be populating and the name of one of its own nested child classes as the starting point of the multipage form.  The outer class must define these “model” and “starting_form” attributes.

As for the child forms, they are subclasses of Django’s regular “ModelForm” class. As such, they have a nested “class Meta” that is used to define which fields should be rendered in the form; set help text, labels, or widgets; or do anything else the regular Django “class Meta” can do.

But there are some special attributes that can be defined on the child forms directly (not in the “class Meta”).  The most important of these for a multipage form is the “next_form_class” attribute, which -- naturally -- contains the name of the next “ChildForm” subclass that will be presented to the user if the current form is submitted with valid inputs.  (The last form in the sequence should not define a “next_form_class” attribute.)

We mentioned above that our model fields had to be declared with blank=True for string fields and null=True for other fields.  A consequence of this is that with a multipage form, a form field cannot be required at the database level.

But we can still make fields required at the form level, and the “ChildForm” class can help with this. Let’s redefine our form as follows:

class JobApplicationForm(MultipageForm):
    model = JobApplication
    starting_form = "Stage1Form"

    class Stage1Form(ChildForm):
        required_fields = ["first_name", "last_name"]
        next_form_class = "Stage2Form"

        class Meta:
            fields = ["first_name", "middle_name", "last_name"]

    class Stage2Form(ChildForm):
        required_fields = "__all__"

        class Meta:
            fields = ["education_level", "year_graduated"]
            help_text = {
                "education_level": "Indicate the highest level of education you have attained"
            }

We’ve added a new “required_fields” attribute to our class definitions. Adding this attribute to your child form will cause the listed fields to be required.  As a convenience, you can also use the string literal “__all__” to indicate that all fields in the “fields” attribute of the nested “Meta” class should be required.  So, on page 1 of the form in the example above, the “first_name” and “last_name” fields will be required, but “middle_name” will not.  On page 2, everything listed under “fields” (“education_level” and “year_graduated”) will be required.

You also have the option to forego defining a “next_form_class” attribute and instead define a “get_next_form_class()” method.  This allows you to branch the form depending on previous user input.  Let’s complete the form we started above by adding three more “ChildForm” subclasses:

    class Stage3Form(ChildForm):
        required_fields = ["prior_company", "prior_experience"]

        class Meta:
            fields = ["prior_company", "prior_experience", "anything_more"]

        def get_next_form_class(self):
            if self.instance.anything_more:
                return "Stage3bForm"
            return "Stage4Form"

    class Stage3bForm(ChildForm):
        next_form_class = "Stage4Form"

        class Meta:
            fields = ["personal_statement"]

    class Stage4Form(ChildForm):
        required_fields = "__all__"

        class Meta:
            fields = ["all_is_accurate"]

Notice that the new “Stage3Form” can advance in one of two different ways.  In this case, the next form will be either a “Stage3bForm” or a “Stage4Form” depending on whether the user has checked the “anything_more” field.  The decision on which direction to take can be based on any information entered by the user up to that point.

The View

Like the model, the view can also be fairly minimal.

job_application/models.py

from multipage_form.views import MultipageFormView
from .forms import JobApplicationForm

class JobApplicationView(MultipageFormView):
    template_name = 'job_application/form_page.html'
    form_class = JobApplicationForm

Most of what this view needs is inherited from the “MultipageFormView” class.  We do need to define a “form_class” attribute.  Naturally, its value here will be the “JobApplicationForm” class defined above.

We’ve also defined a “template_name” attribute.  Naturally enough, this is the template in which the individual form pages will be rendered, and can be used for all pages in the form if you want.  If, however, you need an individual form page to have its own custom template, you can do that, too. There will be more information on that in the next section.

The Template

You can design the template for a multipage form just as you would for any other Django form. Here’s a simplified version of the template in the example project:

         <form id="job-application-form" method="post" action=".">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit">Next</button>
          </form>

As mentioned above, adding a “template_name” attribute to your view is enough to render your form, assuming all your form pages use the same template.  However, an individual child form can also define its own template.  Let’s redefine our “Stage4Form” from above like this:

    class Stage4Form(ChildForm):
        required_fields = "__all__"
        template_name = "job_application/form_page_w_summary.html"

        class Meta:
            fields = ["all_is_accurate"]

By adding a “template_name” attribute to the child form we override the template defined on the view.  In this case, we’re doing it so that we can display a summary of the user’s responses before the final submit.  That can be accomplished with a special template tag, which we’ll discuss in the next section.

Template Tags

This app provides some template tags that may help the user through the multipage process. You can make them available by adding the following line to your template:

{% load multipage_form_tags %}

Now let’s look at each of the custom tags provided:

History

The names of all form pages that the user has completed so far can be displayed as a series of hyperlinks, allowing the user to jump back and forth to make further changes. The example project has:

        {% get_history as links %}
        {% if links %}
        {% for link in links %}
        <ul class="nav">
          <li>{{ link }}</li>
        </ul>
        {% endfor %}
        {% endif %}

Calling the “get_history” tag returns a list of <span> elements, each referencing one form page in the history. Those referencing forms other than the one currently on display to the user will also be hyperlinks to those forms.

Each link in the history chain is rendered in a built-in mini-template. If you want to change how the links are rendered, create a path to an overriding "history_link.html" template in your own app's "templates" directory. It should look like:

       <path/to/your/app>/templates/multipage_form/history_link.html

You can also access the history directly via the “form_history” object in the context. 

Link to Previous Form Page

If instead of a complete history you just want to give the user a link back to the previous form page, a previous attribute is included in the context for all forms other than the starting form. Its value is simply the zero-indexed position of the previous form in the history. You can render such a link in your template like this:

        {% if previous %}
        <a href="?p={{ previous }}">Previous</a>
        {% endif %}

Summary of User Input

It may be useful to display to the user a summary of all form responses before the final "submit" button is clicked. To accomplish this, include the following line in your template:

        {% get_form_summary %}

For greater control over the display of the summary, create an overriding "summary.html" template in your own app's "templates" directory. It should look like:

       <path/to/your/app>/templates/multipage_form/summary.html

The built-in template at “multipage_form/templates/multipage_form/summary.html” can give an idea of the nature of the summary object.

Conclusion

This brings us to the end of our discussion of how to create a multipage form in Django using our multipage form app.

Again, for a working example, you can clone the example project from GitHub.  The working example contains all the code referred to above, and also lets you see submitted JobApplication objects using Django's built-in admin.

Happy coding!


Share , ,
If you're getting even a smidge of value from this post, would you please take a sec and share it? It really does help.