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:
_clean_fields(): Iterates through every field in the form. It first calls the field's ownclean()method (e.g.,EmailField.clean()) and then looks for a method on the form namedclean_<fieldname>()for custom field-specific validation._clean_form(): Calls the form'sclean()method. This is where you implement cross-field validation (e.g., checking if two password fields match)._post_clean(): An internal hook used primarily byModelFormto 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">.