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:
  1. 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.
    When this function completes, the self.cleaned_data dict should be filled with a key/value pair for every field of the form.
  2. 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.
  3. 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
    When this method is finished, the validation process is complete. If no ValidationError has been raised form.is_valid() will return True, so the form (or view) can go on to save self.cleaned_data.
    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

Popular posts from this blog

Handling control characters (escaping) in python for json and mysql

python port sniffer with pcapy and impacket