Search code examples
pythondjangopython-2.7django-1.4

Django model form saves m2m after instance


I am having an issue with the way Django class-based forms save a form. I am using a form.ModelForm for one of my models which has some many-to-many relationships.

In the model's save method I check the value of some of these relationships to modify other attributes:

class MyModel(models.Model):
  def save(self, *args, **kwargs):
    if self.m2m_relationship.exists():
      self.some_attribute = False
    super(MyModel, self).save(*args, **kwargs)

Even if I populated some data in the m2m relationship in my form, I self.m2m_relationship when saving the model and surprisingly it was an empty QuerySet. I eventually found out the following:

The form.save() method is called to save the form, it belongs to the BaseModelForm class. Then this method returns save_instance, a function in forms\models.py. This function defines a local function save_m2m() which saves many-to-many relationships in a form.

Here's the thing, check out the order save_instance chooses when saving and instance and m2m:

instance.save()
save_m2m()

Obviously the issue is here. The instance's save method is called first, that's why self.m2m_relationship was an empty QuerySet. It just doesn't exist yet.

What can I do about it? I can't just change the order in the save_instance function because it is part of Django and I might break something else.


Solution

  • Daniel's answer gives the reason for this behaviour, you won't be able to fix it.

    But there is the m2m_changed signal that is sent whenever something changes about the m2m relationship, and maybe you can use that:

    from django.db.models import signals
    
    @signals.receiver(signals.m2m_changed, sender=MyModel.m2m_relationship.through)
    def handle_m2m_changed(sender, instance, action, **kwargs):
        if action == 'post_add':
            # Do your check here
    

    But note the docs say that instance "can be an instance of the sender, or of the class the ManyToManyField is related to".

    I don't know how that works exactly, but you can try out which you get and then adapt the code.