Skip to main content

Forms and Validation

Forms in this codebase provide a structured way to handle user input, perform validation, and bridge the gap between HTML forms and Python objects or database models. The implementation centers around the Form and ModelForm classes, which automate the repetitive tasks of data conversion and error reporting.

Defining Forms

Forms are defined declaratively by subclassing django.forms.Form or django.forms.ModelForm.

Standard Forms

A standard Form is used when the input does not map directly to a single database model. For example, AuthenticationForm in django/contrib/auth/forms.py defines fields for credentials and includes custom logic to authenticate a user:

class AuthenticationForm(forms.Form):
username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True}))
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
)

def clean(self):
username = self.cleaned_data.get("username")
password = self.cleaned_data.get("password")

if username is not None and password:
self.user_cache = authenticate(
self.request, username=username, password=password
)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)

return self.cleaned_data

ModelForms

ModelForm is a helper that creates a form from a Django model. It automatically generates fields based on the model's definition. The BaseUserCreationForm in django/contrib/auth/forms.py demonstrates how to specify the model and fields using the Meta inner class:

class BaseUserCreationForm(SetPasswordMixin, forms.ModelForm):
class Meta:
model = User
fields = ("username",)
field_classes = {"username": UsernameField}

The Validation Lifecycle

Validation is the process of ensuring that the submitted data is clean and safe. This is triggered by calling is_valid() on a bound form instance.

Internal Flow

When is_valid() is called, it accesses the errors property, which in turn triggers full_clean() (defined in django/forms/forms.py). The full_clean method executes validation in three stages:

  1. _clean_fields(): Iterates through every field in the form. It first calls the field's own clean() method (e.g., EmailField.clean()) and then looks for a method on the form named clean_<fieldname>() for custom field-specific validation.
  2. _clean_form(): Calls the form's clean() method. This is where you implement cross-field validation (e.g., checking if two password fields match).
  3. _post_clean(): An internal hook used primarily by ModelForm to perform model-level validation and uniqueness checks.

Cleaned Data

If validation succeeds, the processed data is stored in the cleaned_data dictionary. If validation fails, errors are stored in the errors attribute (an ErrorDict).

ModelForm Data Persistence

ModelForm provides a save() method to persist the validated data to the database.

The save() Method

The save() method (found in django/forms/models.py) handles both creating new instances and updating existing ones. It supports a commit argument:

  • commit=True (default): Saves the instance to the database immediately and calls _save_m2m() to handle many-to-many relationships.
  • commit=False: Returns an instance that hasn't been saved to the database yet. This is useful if you need to attach extra data to the instance before saving.

Example from BaseUserCreationForm:

def save(self, commit=True):
user = super().save(commit=False)
user = self.set_password_and_save(user, commit=commit)
if commit and hasattr(self, "save_m2m"):
self.save_m2m()
return user

Formsets: Managing Multiple Forms

Formsets allow you to manage multiple instances of the same form on a single page. They are managed by BaseFormSet in django/forms/formsets.py.

ManagementForm

Every formset includes a ManagementForm. This hidden form tracks the total number of forms, the initial number of forms, and the maximum number of forms. It is critical for security; if the ManagementForm is missing or tampered with in a POST request, the formset will raise a ValidationError.

Rendering and File Uploads

BoundField

When you iterate over a form in a template, you are interacting with BoundField objects. A BoundField combines a Field instance with the data from a specific Form instance, allowing it to render itself as HTML and display associated errors.

Handling Files

If a form contains a FileField or ImageField, it must be rendered with the correct encoding. The is_multipart() method in django/forms/forms.py checks if any field in the form requires multipart encoding:

def is_multipart(self):
"""
Return True if the form needs to be multipart-encoded, i.e. it has
FileInput, or False otherwise.
"""
return any(field.widget.needs_multipart_form for field in self.fields.values())

In templates, this is typically used to set <form enctype="multipart/form-data">.