Working in Forms With Django

I still find myself comparing Rails and Django — forms are one thing they handle quite differently, and I really enjoy the way Django does them. Class-based forms are such an easy way to manipulate the information that goes into and out of your forms, and provides a great way to put some of the logic in the form itself, providing a separation from the form and the controller logic.

This only covers user-facing Model forms, not Django Admin forms, or non-model forms. However, once you’ve built model forms, you’ll find you can use the same concepts to build other types of forms.

As always, here are the docs.

A ModelForm is used for creating or editing an instance of an object in the database.

Let’s say we have the following model:

from django.conf import settings
from django.db import models

class Meal(models.Model):
    organizer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='organizer')
    time = models.DateTimeField(null=True, blank=True)
    location = models.CharField(max_length=100)
    required = models.NullBooleanField(default=None)

Here’s an example that shows just a couple things you can do to customize its behavior. We’ll talk through each of them.

from django import forms
from django.forms import fields, CheckboxInput

class MealForm(forms.ModelForm)
    class Meta:
        model = Meal
        fields = ('time', 'location')
    def __init__(self, *args, **kwargs):
        self.organizer = kwargs.pop('organizer')
        super(MealForm, self).__init__(*args, **kwargs)
        if not self.instance:
            self.fields['location'].initial = self.organizer.default_location
        self.fields['required'].widget = CheckboxInput(required=False)
    def clean_time(self):
        time = self.cleaned_data['time']
        # do stuff with the time to put it in UTC based on the user's default timezone and data passed into the form.
    def save(self, *args, **kwargs):
        self.instance.organizer = self.organizer
        meal = super(MealForm, self).save(*args, **kwargs)
        return meal

When creating a model form, you have to tell it which model it’s working with in the Meta class, and tell it which of that Model’s fields to use or exclude. You can be done there if you want — Django will know which widget to use for each field based on the type of field it is (a BooleanField will get a checkbox, a CharField will get a text input, etc.). If the form is editing instead of creating an instance, and an instance is passed in (which we’ll talk about in the next section), the fields will be populated with the current values of those fields. It’s pretty handy and does a lot for you. Here are some of the other things we did in the above form

  • We’re giving the model an organizer. In this case, we’re assuming that the person filling out the form is automatically the organizer of this meal, so we don’t want them to have to fill that out in the form. This assumes an organizer is passed in when the form is instantiated in the view (which, again, we’ll talk about in the next section).
  • Calling super just means that, regardless of these customizations we’re adding here, we want to call the ModelForm’s __init__ class as well. That’s where a lot of the behind the scenes magic happens, and we don’t want to overwrite that.
  • If self.instance is None, that means that this form is being used to create a new instance, instead of editing an existing one. Calling .initial on a form field in this case means that the field will be pre-filled with a default value, that the user can then change. You usually don’t want to give a field an initial value if the form is being used to edit, since the fields will automatically be filled with the current database values.
  • Calling .widget on a field overrides the default provided by the model in the database. In this case, a NullBooleanField will usually render as a dropdown with 3 options: None, True, and False. Here I’m overriding this and telling it to render as a checkbox, which will save as either True or False, because I don’t want my user to be able to select None. You can find all of the widgets available to you here.

Each form has an attribute called cleaned_data on it, that contains the POST information sent over the form, on which validations have already been done, in addition to any custom work you want to do on it. Each field has a clean method that can be customized to do additional logic.

In this example, I would adjust the datetime field to use the user’s default timezone, and then convert it to UTC for storing in the database, so the value represents the right moment in time. But you can do whatever needs to be done to this data to get it in the proper format for you, or to do additional validating on.

When you try to access self.cleaned_data['<field_name>'] in your save method, the data will be the value you’ve returned in this method.

By the time I get here, in the case of a ModelForm, I have an instance (which may or may not have an ID, depending on whether I’m creating or editing). So in my example, I’m setting the organizer value that I retrieved in __init__ on my instance before calling the super method, which actually saves my instance in the database. I then return the instance itself, so I can use it in the view.

from annoying.functions import get_object_or_None
from django.shortcuts import render
from meal.forms import MealForm
from meal.models import Meal
def edit_meal(request, meal_id):
    meal = get_object_or_None(Meal, pk=meal_id)
    if not meal:
        # error handle
    if request.method == "POST":
        form = MealForm(request.POST, instance=meal, organizer=request.user)
        if form.is_valid():
            meal = form.save()
            # maybe addd a success message for your user
   else:
        form = MealForm(instance=meal, organizer=request.user)
    context = {
        'form': form,
        'meal': meal
    }
    return render('<template>')

The main difference between the form you generate from a GET request versus a POST request is whether you pass in the request.POST — this is what the form uses to generate its cleaned_data. If we were drawing up the view to add a new meal instead of editing an existing one, we would also not pass an instance in when initializing the form, and the form would handle the rest.

Calling is_valid() on a form will check two things:

  • Does it have errors — this runs the database validations in the case of a ModelForm to make sure the instance can be saved
  • Is the form ‘bound’ — this really is just checking to see that the form has either data or files. Passing in request.POST, in this case means that the form does have data. request.FILES can be passed in, either in addition to or instead of the request.POST, if there are uploads or other things happening in the form.

Assuming those two things are true, the form is valid, and calling .save() on it will, as we’ve defined the form in the first section, create an instance of a Meal and return it to you, so you can use it in your context.

All that’s left now is to draw up the form, and the template will do most of the work for you. At the most basic, it might look something like this:

<form action="{% url 'meal:edit' meal.id %}" method="post">
    {% csrf_token %}
    {  }
    <input type="submit" value="Submit" />
</form>

The { } will draw up the inputs for you with the attributes you specified in your MealForm, above, and hitting the “Submit” button will take you to the edit_meal action (This assumes you’ve got the associated url, of course). Voilà!