Form Basics§

Up until this point we’ve been using forms without really needing to be aware of it. A Django Form is responsible for taking some user input, validating it, and turning it into Python objects. They also have some handy rendering methods, but I consider those sugar: the real power is in making sure that input from your users is what it says it is.

The Generic Views, specifically the ones we’ve been using, all operate on a particular model. Django is able to take the model definition that we’ve created and extrapolate a Form from it. Django can do this because both Models and Forms are constructed of fields that have a particular type and particular validation rules. Models use those fields to map data to types that your database understands; Forms use them to map input to Python types [1]. Forms that map to a particular Model are called ModelForms; you can think of them as taking user input and transforming it into an instance of a Model.

[1]While I’m referring to them both as fields, they’re really completely different implementations. But the analogy holds.

Adding Fields to the Form§

So what if we want to add a field to our form? Say, we want to require confirmation of the email address. In that case we can create a new form, and override the default used by our views.

First, in the contacts app directory, we’ll create a new file, forms.py.

from django import forms
from django.core.exceptions import ValidationError

from contacts.models import Contact


class ContactForm(forms.ModelForm):

    confirm_email = forms.EmailField(
        label="Confirm email",
        required=True,
    )

    class Meta:
        model = Contact

    def __init__(self, *args, **kwargs):

        if kwargs.get('instance'):
            email = kwargs['instance'].email
            kwargs.setdefault('initial', {})['confirm_email'] = email

        return super(ContactForm, self).__init__(*args, **kwargs)

Here we’re creating a new ModelForm; we associate the form with our model in the Meta inner class.

We’re also adding an additional field, confirm_email. This is an example of a field declaration in a model. The first argument is the label, and then there are additional keyword arguments; in this case, we simply mark it required.

Finally, in the constructor we mutate the initial kwarg. initial is a dictionary of values that will be used as the default values for an unbound form. Model Forms have another kwarg, instance, that holds the instance we’re editing.

Overriding the Default Form§

We’ve defined a form with the extra field, but we still need to tell our view to use it. You can do this in a couple of ways, but the simplest is to set the form_class property on the View class. We’ll add that property to our CreateContactView and UpdateContactView in views.py.

import forms
...
class CreateContactView(CreateView):

    model = Contact
    template_name = 'edit_contact.html'
    form_class = forms.ContactForm
class UpdateContactView(UpdateView):

    model = Contact
    template_name = 'edit_contact.html'
    form_class = forms.ContactForm

If we fire up the server and visit the edit or create pages, we’ll see the additional field. We can see that it’s required, but there’s no validation that the two fields match. To support that we’ll need to add some custom validation to the Form.

Customizing Validation§

Forms have two different phases of validation: field and form. All the fields are validated and converted to Python objects (if possible) before form validation begins.

Field validation takes place for an individual field: things like minimum and maximum length, making sure it looks like a URL, and date range validation are all examples of field validation. Django doesn’t guarantee that field validation happens in any order, so you can’t count on other fields being available for comparison during this phase.

Form validation, on the other hand, happens after all fields have been validated and converted to Python objects, and gives you the opportunity to do things like make sure passwords match, or in this case, email addresses.

Form validation takes place in a form’s clean() method.

class ContactForm(forms.ModelForm):
...
    def clean(self):

        if (self.cleaned_data.get('email') !=
            self.cleaned_data.get('confirm_email')):

            raise ValidationError(
                "Email addresses must match."
            )

        return self.cleaned_data

When you enter the clean method, all of the fields that validated are available in the cleaned_data dictionary. The clean method may add, remove, or modify values, but must return the dictionary of cleaned data. clean may also raise a ValidationError if it encounters an error. This will be available as part of the forms’ errors property, and is shown by default when you render the form.

Note that I said cleaned_data contains all the fields that validated. That’s because form-level validation always happens, even if no fields were successfully validated. That’s why in the clean method we use cleaned_data.get('email') instead of cleaned_data['email'].

If you visit the create or update views now, we’ll see an extra field there. Try to make a change, or create a contact, without entering the email address twice.

Controlling Form Rendering§

Our templates until now look pretty magical when it comes to forms: the extent of our HTML tags has been something like:

<form action="{{ action }}" method="POST">
  {% csrf_token %}
  <ul>
    {{ form.as_ul }}
  </ul>
  <input type="submit" value="Save" />
</form>

We’re living at the whim of form.as_ul, and it’s likely we want something different.

Forms have three pre-baked output formats: as_ul, as_p, and as_table. If as_ul outputs the form elements as the items in an unordered list, it’s not too mysterious what as_p and as_table do.

Often, though, you need more control. For those cases, you can take full control. First, a form is iterable; try replacing your call to {{form.as_ul}} with this:

{% for field in form %}
{{ field }}
{% endfor %}

As you can see, field renders as the input for each field in the form. When you iterate over a Form, you’re iterating over a sequence of BoundField objects. A BoundField wraps the field definition from your Form (or derived from the ModelForm) along with any data and error state it may be bound to. This means it has some properties that are handy for customizing rendering.

In addition to supporting iteration, you can access an individual BoundField directly, treating the Form like a dictionary:

{{ form.email }}

Consider the following alternative to edit_contact.html.

{% extends "base.html" %}

{% block content %}

{% if contact.id %}
<h1>Edit Contact</h1>
{% else %}
<h1>Add Contact</h1>
{% endif %}

<form action="{{ action }}" method="POST">
  {% csrf_token %}
  {% if form.non_field_errors %}
    <ul>
      {% for error in form.non_field_errors %}
        <li>{{ error }}</li>
      {% endfor %}
    </ul>
  {% endif %}
  {% for field in form %}
  <div id="{{ field.auto_id }}_container">
    {{ field.help_text }}
    <div>
      {{ field.label_tag }} {{ field }}
    </div>
    <div id="{{ field.auto_id }}_errors">
      {{ field.errors }}
    </div>
  </div>
  {% endfor %}

  <input id="save_contact" type="submit" value="Save" />
</form>

{% if contact.id %}
<a href="{% url "contacts-edit-addresses" pk=contact.id %}">
  Edit Addresses
</a>
<a href="{% url "contacts-delete" pk=contact.id %}">Delete</a>
{% endif %}

<a href="{% url "contacts-list" %}">back to list</a>

{% endblock %}

In this example we see a few different things at work:

  • field.auto_id to get the automatically generated field ID

  • Combining that ID with _container and _errors to give our related elements names that consistently match

  • Using field.label_tag to generate the label. label_tag adds the appropriate for property to the tag, too. For the last_name field, this looks like:

    <label for="id_last_name">Last name</label>
    
  • Using field.errors to show the errors in a specific place. The Django Form documentation has details on further customizing how errors are displayed.

  • Finally, field.help_text. You can specify a help_text keyword argument to each field when creating your form, which is accessible here. Defining that text in the Form definition is desirable because you can easily mark it up for translation.

Testing Forms§

It’s easy to imagine how you’d use the LiveServerTestCase to write an integration test for a Form. But that wouldn’t just be testing the Form, that’d be testing the View, the URL configuration, and probably the Model (in this case, at least). We’ve built some custom logic into our form’s validator, and it’s important to test that and that alone. Integration tests are invaluable, but when they fail there’s more than one suspect. I like tests that fail with a single suspect.

Writing unit tests for a Form usually means crafting some dictionary of form data that meets the starting condition for your test. Some Forms can be complex or long, so we can use a helper to generate the starting point from the Form’s initial data.

Rebar is a collection of utilities for working with Forms. We’ll install Rebar so we can use the testing utilities.

(tutorial)$ pip install rebar

Then we can write a unit test that tests two cases: success (email addresses match) and failure (they do not).

from rebar.testing import flatten_to_dict
from contacts import forms
...
class EditContactFormTests(TestCase):

    def test_mismatch_email_is_invalid(self):

        form_data = flatten_to_dict(forms.ContactForm())
        form_data['first_name'] = 'Foo'
        form_data['last_name'] = 'Bar'
        form_data['email'] = 'foo@example.com'
        form_data['confirm_email'] = 'bar@example.com'

        bound_form = forms.ContactForm(data=form_data)
        self.assertFalse(bound_form.is_valid())

    def test_same_email_is_valid(self):

        form_data = flatten_to_dict(forms.ContactForm())
        form_data['first_name'] = 'Foo'
        form_data['last_name'] = 'Bar'
        form_data['email'] = 'foo@example.com'
        form_data['confirm_email'] = 'foo@example.com'

        bound_form = forms.ContactForm(data=form_data)
        self.assert_(bound_form.is_valid())

An interesting thing to note here is the use of the is_valid() method. We could just as easily introspect the errors property that we used in our template above, but in this case we just need a Boolean answer: is the form valid, or not? Note that we do need to provide a first and last name, as well, since those are required fields.

Review§

  • Forms take user input, validate it, and convert it to Python objects
  • Forms are composed of Fields, just like Models
  • Fields have validation built in
  • You can customize per-field validation, as well as form validation
  • If you need to compare fields to one another, you need to implement the clean method
  • Forms are iterable over, and support dictionary-like access to, the bound fields
  • A Bound Field has properties and methods for performing fine-grained customization of rendering.
  • Forms are unit testable; Rebar has some utilities to help with testing large forms.