Understanding how saving forms works

Let’s begin with what I was trying to achieve.
I’ve got a model containing a boolean field and a foreign key:

class MyModel(models.Model):
    name = models.CharField(max_length=200)

class MyMark(models.Model):
    chkb = models.BooleanField(default=True)
    my_model = models.ForeignKey(MyModel, null=True, blank=True, on_delete=models.CASCADE)

I’m displaying a CreateView/UpdateView for MyMark model. What I wanted to do is to set my_model value to None if the chkb value is False, even if my_model value is selected by the user in the form.
Originally I tried to do this as follows:

class MyMarkCreate(CreateView):
    model = MyMark
    fields = "__all__"

    ...

    def form_valid(self, form):
        if not form.cleaned_data["chkb"]:
            form.cleaned_data.pop("my_model")
        return super().form_valid(form)

but that didn’t work as I expected it to - created/updated MyMark object still had non-empty my_model value. I expected the object to be created from form.cleaned_data in super().form_valid(form), but apparently that’s not what’s happening.

I poked around Django code a bit and found that the model instance is created in django.forms.models.construct_instance(), which is called from BaseModelForm._post_clean(), building instance from cleaned_data - so before I alter cleaned_data in my form_valid.

This StackOverflow answer suggests calling form.save(commit=False) manually and setting field’s value as needed:

class MyMarkUpdate(UpdateView):
    ...
    def form_valid(self, form):
        if not form.cleaned_data["chkb"]:
            my_mark = form.save(commit=False)
            my_mark.my_model = None
            my_mark.save()

        print(form.cleaned_data)
        return super().form_valid(form)

That works, but then I don’t understand why my manually set value is not automatically overwritten again in super().form_valid(form). It should call form.save() again after all - or is it not what’s happening? From the print I see that form.cleaned_data still contains a value for my_model field.

So, if anyone could help explain what’s happening here, I’d be grateful.

And another question - is there a better way to achieve my original goal? Maybe writing a custom clean() method, dropping my_model from cleaned_data would be better?

Because form_valid itself does not create or modify an instance, that work is done earlier in the process.

Your variable my_mark is a reference to the form.instance object created by the form. It is resaving the object - but it’s what you’ve already modified. Nothing is happening that would change a value in that instance.

Are there other ways? Sure. Are they any better? :man_shrugging:

1 Like

This is the point I overlooked. Thanks, that explains it!

Oh, and there is one more thing I meant to mention but forgot.

If you look through what the CBVs are doing, you’ll see that form_valid essentially boils down to a save and return HttpResponseRedirect.

There’s really no need for you to call super().form_valid() in a CBV if you’re doing the save. You can return your own HttpResponseRedirect and avoid the duplicate save.

1 Like

Fair enough, thanks again!