Django field, form and model validation process
Django's form and field validation process can be best understood by looking at the code in django's BaseForm class (in django.forms.forms) - see below. See also this blog post for a graphic overview.
The form.full_clean() method is called by django at the start of the validation process (by form.is_valid(), usually immediately after the view receives the posted data). full_clean() performs a 3 phase validation, with each phase corresponding to a call of one of the protected methods below:
The form.full_clean() method is called by django at the start of the validation process (by form.is_valid(), usually immediately after the view receives the posted data). full_clean() performs a 3 phase validation, with each phase corresponding to a call of one of the protected methods below:
- self._clean_fields() : Field validation
This method iterates over the form fields:- For each field, it calls the Widget's value_from_datadict() method, passing the complete post data in a QueryDict. The Widget's task is to retrieve its own value from this QueryDict and return it.
- The returned value is then passed to the Field's own clean() method. This method does the initial validation (e.g. checking if the value is not empty), without touching the db.
- If the Field's validation fails, a ValidationError is raised.
If the Field's validation succeeds, the return value of the Field.clean() method is stored in the Form's self.cleaned_data dict (key = name of the field). - The function then checks if the Form subclass has implemented a clean_{fieldname} method, and calls it. This function can do further validation, based on the value in self.cleaned_data: if validation succeeds, the return value is used to overwrite the Field's value in the self.cleaned_data dict.
- self._clean_form() : Form validation
This method just calls the Form's clean() method. The Form sub-class can override this clean() method to perform validation that requires access to multiple fields at once, using its self.cleaned_data dict. The Form's clean() function either raises a ValidationError or returns a full self.cleaned_data dict. In the latter case, self.clean_form() overwrites its self.cleaned_data dict with the return value. - self._post_clean() : Model validation, in case the form is a ModelForm
This corresponds to a call of BaseModelForm._post_clean() (in django.forms.models) which repeats the validation process for the Data Model instance. This results in consecutive calls to the following Model instance methods:- Model.clean_fields(exclude=None) : does the validation for each of the Model's fields
- Model.clean() : allows the Model instance to provide custom validation, and to modify attributes on your model if desired.
- Model.validate_unique() : validates all uniqueness constraints on your model
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
self._post_clean()
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
def _clean_form(self):
try:
self.cleaned_data = self.clean()
except ValidationError as e:
self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
def _post_clean(self):
"""
An internal hook for performing additional cleaning after form cleaning
is complete. Used for model validation in model forms.
"""
pass
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field named '__all__'.
"""
return self.cleaned_data
def has_changed(self):
"""
Returns True if data differs from initial.
"""
return bool(self.changed_data)
def _get_changed_data(self):
if self._changed_data is None:
self._changed_data = []
# XXX: For now we're asking the individual widgets whether or not the
# data has changed. It would probably be more efficient to hash the
# initial data, store it in a hidden field, and compare a hash of the
# submitted data, but we'd need a way to easily get the string value
# for a given field. Right now, that logic is embedded in the render
# method of each widget.
for name, field in self.fields.items():
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if not field.show_hidden_initial:
initial_value = self.initial.get(name, field.initial)
else:
initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget()
initial_value = hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name)
if field.widget._has_changed(initial_value, data_value):
self._changed_data.append(name)
return self._changed_data
changed_data = property(_get_changed_data)
Comments