Hi all

We encountered a strange issue today. When a user submits a formset
that is empty, we call is_valid() on the formset, expecting the
clean() method to be called. We then subsequently get errors later on,
because the formset did not call self.clean(), and therefore it did
not populate the fields we require on the formset.

This is probably easier to explain with an example:

from django import forms
from django.forms.formsets import BaseFormSet, formset_factory

class EmptyFsetWontValidate(BaseFormSet):
  def clean(self):
    raise forms.ValidationError, "Clean method called"

class SimpleForm(forms.Form):
  name = forms.CharField()

EmptyFsetWontValidateFormset = formset_factory(SimpleForm, extra=0,
formset=EmptyFsetWontValidate)
formset = EmptyFsetWontValidateFormset({'form-INITIAL_FORMS':'0',
'form-TOTAL_FORMS':'0'})
formset2 = EmptyFsetWontValidateFormset({'form-INITIAL_FORMS':'0',
'form-TOTAL_FORMS':'1', 'form-0-name':'bah' })

>>> formset.is_valid()
True
>>> formset2.is_valid()
False

I would have expected both to be invalid.

The reason it doesn't mark both as invalid is clear enough.
BaseFormSet::is_valid() and BaseFormSet::errors looks like so:

    def _get_errors(self):
        """
        Returns a list of form.errors for every form in self.forms.
        """
        if self._errors is None:
            self.full_clean()
        return self._errors
    errors = property(_get_errors)

    def is_valid(self):
        """
        Returns True if form.errors is empty for every form in self.forms.
        """
        if not self.is_bound:
            return False
        # We loop over every form.errors here rather than short
circuiting on the
        # first failure to make sure validation gets triggered for every form.
        forms_valid = True
        for i in range(0, self.total_form_count()):
            form = self.forms[i]
            if self.can_delete:
                # The way we lookup the value of the deletion field here takes
                # more code than we'd like, but the form's cleaned_data will
                # not exist if the form is invalid.
                field = form.fields[DELETION_FIELD_NAME]
                raw_value = form._raw_value(DELETION_FIELD_NAME)
                should_delete = field.clean(raw_value)
                if should_delete:
                    # This form is going to be deleted so any of its errors
                    # should not cause the entire formset to be invalid.
                    continue
            if bool(self.errors[i]):
                forms_valid = False
        return forms_valid and not bool(self.non_form_errors())

It loops over each form in the list, and sets forms_valid to False if
any one of them has errors in self.errors[i]. self.errors is a
property, which runs self.full_clean() if unpopulated. The problem is
that if there are no forms in the fieldset, then self.errors is never
examined, and hence self.full_clean() is never called.

I think this is a bug, especially as the return value of
formset.is_valid() can depend on whether formset.errors has been
populated. Eg:

>>> formset3 = EmptyFsetWontValidateFormset({'form-INITIAL_FORMS':'0', 
>>> 'form-TOTAL_FORMS':'0'})
>>> formset3.is_valid()
True
>>> formset3.errors
[]
>>> formset3.is_valid()
False


Cheers

Tom

--

You received this message because you are subscribed to the Google Groups 
"Django users" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-users?hl=en.


Reply via email to