Combining Inherited Django Forms in the Same FormView

This week, I discovered the power of Django’s generic editing views and was eager to try them out. I had two forms; one inheriting from the other. I could have them each in their own view, but for a better user experience, I wanted it all to happen on one page by allowing the user to toggle the extra fields. Today, I’m going to show you how to combine inherited forms or models in the same view with some Django magic and a little Javascript.

Example

The example application we’ll be creating today is a simple job application submission page. You can either fill out an application for yourself, or you can fill it out on behalf of someone else as a referral.

The End Result

Before we look at any code, this is what the page when initially loaded will look like:

When the user checks the “Is this a referral?” checkbox, the two extra fields that are required for referrals pop up:

Lastly, the user is redirected to a simple success page when they make a successful submission:

The Models

from django.db import models


class JobApplication(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField(max_length=50)
    relevant_experience = models.BooleanField()


class JobReferral(JobApplication):
    referrer_name = models.CharField(max_length=50)
    referrer_email = models.EmailField(max_length=50)

A job application requires the person’s name, email, and a indication of whether they have relevant experience or not (on the honor system, of course). A job referral requires all of the same fields, except they also need the referrer’s name and email.

The Forms

class JobApplicationForm(forms.ModelForm):

    class Meta:
        model = JobApplication
        fields = ['name', 'email', 'relevant_experience']


class JobReferralForm(forms.ModelForm):
    is_referral = forms.BooleanField(
        label='Is this a referral?'
    )

    class Meta:
        model = JobReferral

        fields = [
            'name', 'email', 'relevant_experience', 'referrer_name',
            'referrer_email'
        ]

In our example, we don’t need to do much for the forms, thanks to Django’s ModelForm. In your own code, models may not directly map to forms, and the forms themselves may be inherited. Don’t worry, the same concepts will apply.

The extra is_referral field will be used to determine whether the user is submitting a referral or not. It may seem weird that this is in JobReferralForm, but that will make more sense in a moment.

The Template

{% load static %}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Submit an Application</title>

    <style>
      .hidden {
        display: none;
      }
    </style>
  </head>
  <body>
    <h3>
      Submit a Job Application
    </h3>
    <form id="application-form" action="{% url 'Submit Job Application' %}" method="POST">
      {% csrf_token %}
      <p>
        {{ form.non_field_errors }}
      </p>
      <p>
        {{ form.name.errors }}
        {{ form.name.label_tag }}
        {{ form.name }}
      </p>
      <p>
        {{ form.email.errors }}
        {{ form.email.label_tag }}
        {{ form.email }}
      </p>
      <p>
        {{ form.relevant_experience.errors }}
        {{ form.relevant_experience.label_tag }}
        {{ form.relevant_experience }}
      </p>
      <p>
        {{ form.is_referral.errors }}
        {{ form.is_referral.label_tag }}
        {{ form.is_referral }}
      </p>
      <div class="referral-fields hidden">
          <p>
            {{ form.referrer_name.errors }}
            {{ form.referrer_name.label_tag }}
            {{ form.referrer_name }}
          </p>
          <p>
            {{ form.referrer_email.errors }}
            {{ form.referrer_email.label_tag }}
            {{ form.referrer_email }}
          </p>
      </div>
      <input type="submit" name="submit" />
    </form>

    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
      crossorigin="anonymous"></script>
    <script
      src="https://cdn.jsdelivr.net/npm/jquery-validation@1.17.0/dist/jquery.validate.js"></script>
    <script src="{% static 'submit_application.js' %}"></script>
  </body>
</html>

In the template, we make all of the shared fields visible, along with the is_referral field. The referral fields are grouped separately and are initially hidden.

The Views

from django.http import HttpResponse
from django.views.generic.edit import FormView
from django.urls import reverse_lazy


class SubmitJobApplicationView(FormView):
    success_url = reverse_lazy('Job Application Submission Success')
    template_name = 'submit_application.html'

    def get_form_class(self):
        # Always use JobReferralForm for GET requests,
        # so you can render all of the fields for either model.
        if self.request.method == 'GET':
            return JobReferralForm
        else:
            # On a POST request, return the form class used for validation.
            # Use JobApplicationForm if this is not a referral. Otherwise,
            # use JobReferralForm.
            is_referral = self.request.POST.get('is_referral')
            if is_referral:
                return JobReferralForm
            else:
                return JobApplicationForm

    def form_valid(self, form):
        # Save the form data to a new model instance in the database.
        form.save()
        return super().form_valid(form)

def application_success(request):
    return HttpResponse('Thanks for the application!')

This is where the magic happens. Using get_form_class() is the secret here. Overriding this FormView method allows you to dynamically determine the form class to use at the time the view is called, based on the request.

On a GET request, you always want to use the subclass (JobReferralForm). This is because on a GET request in a FormView, you are only rendering the form, so you want to render all possible fields the user may need. That is why we added the extra is_referral field to JobReferralForm instead of JobApplicationForm.

On a POST request, the user’s data has been submitted and is ready to be saved to the database. You need to determine which form to use for validation based on the request data. In our example, if is_referral is True, we need to use JobReferralForm to validate a referral. Otherwise, we use JobApplicationForm to validate a basic job application.

Lastly, don’t forget to override form_valid() to save the form. ModelForm‘s save() method is very handy, as it saves the valid form’s data directly to the database.

The Javascript

Lastly, we need a bit of Javascript to make the referral fields appear/disappear when the checkbox is clicked. For this example, I used jQuery for simplicity, but you can replicate this with whatever frontend technology you prefer. Also, since default HTML5 validation isn’t able to ignore hidden required fields, I used the jQuery Validation plugin.

var $referralFields = $( ".referral-fields" );

// Hide/show referral fields based on the user's selection.
$( "#id_is_referral" ).change( function() {
    if ( $( this ).is( ":checked" ) ) {
        $referralFields.removeClass( "hidden" );
    } else {
        $referralFields.addClass( "hidden" );
    }
} ).change();  // Trigger an initial change() to set the initial state.

// Set up validation on the form, ignoring hidden fields.
$( "#application-form" ).validate( {
    ignore: "input:not(:visible)"
} );

Summary

By using Django’s FormView and overriding the get_form_class() method, you can create views that dynamically use different forms and save to different models based on user input. You can leverage this powerful feature to combine multiple inherited forms on the same page, but the possibilities extend much further.

The full example code can be found on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.