Hi,

I've come across some interesting behaviour if you intentionally supply bad 
data to a model formset whose forms use CheckboxInputs. Basically, it's 
possible to have custom validation code on the form class which ensures the 
boolean field's input is False, have this validation pass, but then the 
instance silently fails to save to the database when you call formset.save() 
when an instance's existing value is True.

The way to do this is to provide a '0' as input on the form submit, rather than 
omitting the field entirely, as a proper checkbox widget would do.

This is more easily explained with code:


class M(models.Model):
    flag = models.BooleanField()


class DefaultModelForm(forms.ModelForm):
    # This form will get an auto-generated BooleanField, which uses
    # the CheckboxInput widget.
    class Meta:
        model = M

    def clean(self):
        # Now, because BooleanField.to_python knows about '0',
        # cleaned_data actually contains False. '0' and '1' can
        # be posted by RadioSelect widgets.
        cleaned_data = super(DefaultModelForm, self).clean()
        if cleaned_data.get('flag', None) != False:
            raise ValidationError, u'Flag must be false!'
        return cleaned_data


class SimpleTest(TestCase):

    def setUp(self):
        # Let's set up a signal handler so we can tell whether the model was 
saved.
        self.save_count = collections.defaultdict(int)

        def saved(sender, **kwargs):
            self.save_count['count'] += 1
        signals.post_save.connect(saved, sender=M, dispatch_uid='M')

        self.m = M.objects.create(flag=True)

        # Prove our plumbing works
        self.assertEqual(1, self.save_count['count'])


    def test_checkbox(self):
        formset_class = forms.models.modelformset_factory(
            M,
            max_num=1,
            form=DefaultModelForm
        )
    
        # Note that 'form-0-flag' being 0 is what a radio button
        # would normally provide.
        formset = formset_class({
            'form-INITIAL_FORMS': '1',
            'form-TOTAL_FORMS': '1',
            'form-0-id': str(self.m.pk),
            'form-0-flag': '0',
        }, queryset=M.objects.all())

        # Uh-oh - this now validates as True, because the BooleanField 
successfully
        # parsed '0' as False, but the CheckboxWidget doesn't know what to do 
with
        # '0'.
        formset.is_valid()


        # Since all the data looks OK, we go ahead and try to save. To compound
        # the problem, the CheckboxWidget sees that the initial value from the
        # model was True, and interprets '0' as bool('0') == True, so doesn't
        # think that the data has changed. The model therefore never actually
        # gets saved...
        formset.save()

        # If it saved properly, our save count should have gone up by one. 
Sadly,
        # it didn't, so this test fails.
        self.assertEqual(2, self.save_count['count'])

        # So - despite is_valid() assuring us that all our data was OK, saving 
the
        # model silently failed.

I've tried this with Django 1.2.3 and trunk r14735. 

I'm not quite sure what the bug is here, if there is one; but it seem wrong 
that you can write validation code that explicitly checks cleaned_data for a 
boolean to be False, have that validation pass, and then for the underlying 
model to not be saved when the formset save() is called.

(For the curious - we discovered this while writing tests for a Piston API that 
uses formsets to validate data, and accidentally passed a '0' instead of 
omitting the field in the test.)

Should I raise a bug for this one?

Cheers,
Dan
--
Dan Fairs | [email protected] | www.fezconsulting.com


-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" 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-developers?hl=en.

Reply via email to