3

I want to trigger some behaviour whenever a many-to-many relationship changes, but I'm unsure what signal setup is best for capturing changes to that relationship that come about due to the deletion of one side of the relationship. m2m_changed does not seem to fire in this case, nor do the regular post_save and post_delete signals seem to be applicable to the through model?

My current solution is to listen to pre_delete on the models which compose either side of the relationship and then clear the relationship within that signal in order to trigger a m2m_changed. I'm surprised that I have to do this though, and feel I've got something wrong.

What am I missing here? And if I am not missing anything why is this necessary (ie why is no such signal fired by default)?

Code example:

class ResearchField(models.Model):

    name = models.CharField(unique=True, max_length=200)

class Researcher(models.Model):

    name = models.CharField(max_length=200)
    research_fields = models.ManyToManyField(ResearchField, blank=True)

@receiver(m2m_changed, sender=Researcher.research_fields.through)
def research_fields_changed(sender, instance, action, **kwargs):
    # need to do something important here
    print('m2m_changed', action)

volcanology = ResearchField.objects.create(name='Volcanology')
researcher = Researcher.objects.create(name='A. Volcanologist')

researcher.research_fields.add(volcanology)
>>> m2m_changed pre_add
>>> m2m_changed post_add

This m2m_changed signal fires as expected when the relationship is removed from either side:

researcher.research_fields.remove(volcanology)
# or equally volcanology.researcher_set.remove(researcher)
>>> m2m_changed pre_remove
>>> m2m_changed post_remove

However no pre_remove or post_remove m2m_changed signals fire if I simply delete one side of the relationship, despite the Django delete output indicating that an instance of the through model was removed:

# with the relationship intact
volcanology.delete()
>>> (2, {'ResearchField': 1, 'Researcher_research_fields': 1})

At this point I tried:

@receiver(post_delete, sender=Researcher.research_fields.through)
def through_model_deleted(sender, instance, **kwargs):
    print('through model deleted')

But this never fires?

As a result my current solution is to have:

@receiver(pre_delete, sender=ResearchField)
def research_field_deleted(sender, instance, **kwargs):
    instance.researcher_set.clear()

To custom enforce that when Researcher.research_fields.through objects are being deleted via a cascade from a deleted ResearchField the m2m_changed signal will fire after all. However as I said at the top it feels like I've missed something that this is necessary?

  • Possible duplicate of [Django: m2m\_changed not fired when end of relation is deleted](https://stackoverflow.com/questions/31937208/django-m2m-changed-not-fired-when-end-of-relation-is-deleted) – Broc Harcourt Oct 24 '17 at 02:49

1 Answers1

3

I missed this when posting my question but it seems this is an open bug in Django https://code.djangoproject.com/ticket/17688.