In a few recent Django web development projects at Fusionbox, it has been necessary to create forms in which certain fields are conditionally required. For example, if a user selects a certain item from a drop down list, more fields might appear to gather more information about that particular selection. Let's say that the form asks for shipping information. A user can select either "include shipping" or "no shipping". If the user selects "include shipping", the form asks them whether they want to ship to a business or residential address.
First, we define our Django model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from django.db import models
class ShippingInfo(models.Model):
SHIPPING_DESTINATION_CHOICES = (
('residential', "Residential"),
('commercial', "Commercial"),
)
shipping = models.BooleanField()
shipping_destination = models.CharField(
max_length=15,
choices=SHIPPING_DESTINATION_CHOICES,
blank=True
)
|
A few notes about this snippet:
SHIPPING_DESTINATION_CHOICES
has both single quotes and double quotes in the same tuple. It's a popular opinion at Fusionbox that strings that are meant to be seen by the user use double quotes, and that all other strings should use single quotesshipping_destination
has the argumentblank=True
passed to it, but retains its default argument ofnull=True
. Django convention is to store an empty value as the empty string, and not asNULL
. In fact, Django will convertNULL
values to the empty string when retrieving values from its database.
Django doesn't support conditional constraints of the kind we would want to enforce for our model yet. While it is possible to write the raw SQL required to make the constraints work, it's unmaintainable and will not yield meaningful errors in the application. As a result, we'll rely on our form validation to keep our database consistent. We'll take care of this in our form's clean
method.
1 2 3 4 5 6 7 8 9 10 11 12 13 | def clean(self):
shipping = self.cleaned_data.get('shipping')
if shipping:
msg = forms.ValidationError("This field is required.")
self.add_error('shipping_destination', msg)
else:
# Keep the database consistent. The user may have
# submitted a shipping_destination even if shipping
# was not selected
self.cleaned_data['shipping_destination'] = ''
return self.cleaned_data
|
This method works if you have only one or a few conditionally required fields, but as soon as you have more, this code cries out to be pulled into its own function.
1 2 3 4 5 6 | def fields_required(self, fields):
"""Used for conditionally marking fields as required."""
for field in fields:
if not self.cleaned_data.get(field, ''):
msg = forms.ValidationError("This field is required.")
self.add_error(field, msg)
|
Now our clean method looks like this:
1 2 3 4 5 6 7 8 9 | def clean(self):
shipping = self.cleaned_data.get('shipping')
if shipping:
self.fields_required(['shipping_destination'])
else:
self.cleaned_data['shipping_destination'] = ''
return self.cleaned_data
|
Not much of a simplification here, since we only have one conditionally required field, but it makes a huge difference in a large form.
The last part is to indicate to the user that these fields are conditionally required. The form should not confuse the user with questions that don't make sense ("I said no shipping, why are they asking me about the shipping destination?"), and it should not waste their time by asking for information it doesn't need. A great way to do this is to hide the fields that aren't required with Javascript. The principle of progressive enhancement suggests that a form should work without Javascript however. For this form to work without Javascript, the fields must all initially be visible, and ideally they should have help text indicating that they are conditional fields. Let's change our field to have such help text.
1 2 3 4 5 6 | shipping_destination = models.CharField(
max_length=15,
choices=SHIPPING_DESTINATION_CHOICES,
blank=True,
help_text="Only required if 'shipping' is selected.",
)
|
Now this form will work for someone without Javascript without much work on the part of the developer, but this form can still be made better for the majority of users who choose to run Javascript. For this, we hide all of the conditionally required fields and display them when the fields upon which they depend change.
1 2 3 4 5 6 7 8 9 10 | var conditional_fields = $("div.shipping_destination");
conditional_fields.hide();
$(".shipping").change(function() {
if ($(this).prop('checked') === 'checked') {
conditional_fields.show();
} else {
conditional_fields.hide();
}
});
|
There you have it! Conditional forms with Django!
If you want to keep learning Django web development, Fusionbox provides Django training for internal teams.