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.