Django – How to add ManytoMany model inside a post_save signal

I cannot tell you how much hair I tore out in trying to solve this problem, but it quite a lot. I was tearing hair out everyday 8.am to 5a.m (the next day) for 3 days.

I guess what I should have done was just debug django…but buy “print()”ing all over the place, I could see that everything should work as expected, but unfortunately…it didn’t.

See an extract of my script below. I have left all the “thrash” in too 🙂

def set_m2m_contact_usergroup(sender, instance, created, **kwargs):
    
    if created:
        #ugidinst = instance.kit_user.cou_group #user_group_id_of_instance
        #co = Contact.objects.get(pk = instance.slug)
        #ug = CoUserGroup.objects.get(pk = ugidinst)
        #print(instance.kit_user.cou_group == ug)
        instance.cou_group.add(instance.kit_user.cou_group)
        #instance.save()
        #print(instance.cou_group.all())
        #instance.cou_group.add(instance.kit_user.cou_group)
        print("M2M added")
        #instance.save()
        print("Saving M2M")
        #else:
        #print(" --- Created")

You see, this function as supposed to be called by

post_save.connect(set_m2m_contact_usergroup, sender=Contact) 

when the Contact object was saved. See (part of) the model class below

class Contact(models.Model):

    slug = RandomSlugField(length=9, exclude_lower=True, primary_key=True)
    ...    
    kit_user = models.ForeignKey(KITUser, models.PROTECT)
    cou_group = models.ManyToManyField('CoUserGroup', blank=True)
    ...

    def __str__(self):
        return self.first_name

What I’m trying to do in simple terms is set the cou_group field when a new object is created. Because M2Ms can only be created after the related and relating models have been created (in order not to run into integrity error), I dediced to do it in a post_save.
Problem is that no matter what I tried, my m2m was not getting saved.
It turns out that the problem has to do with transactions and threads and the way they work together.
The save()ing, pre_save, post_save etc are atomic and all run in a single thread. M2M will not work within another transaction – at least that’s how I understand it. Anyway, something new was added in Django 1.9 and later to solve this problem. transactio.on_commit.
According to the documentation,

Sometimes you need to perform an action related to the current database transaction, but only if the transaction successfully commits. Examples might include a Celery task, an email notification, or a cache invalidation.

Eventually, this is how my working code looks:

def set_m2m_contact_usergroup(sender, instance, created, **kwargs):
    
    if created:
        transaction.on_commit(
             lambda: instance.cou_group.add(instance.kit_user.cou_group)
        )

That’s all…you won’t believe it.

Thanks to all the SO QnAs that helped guide me until I eventually (while having breakfast) googled, “django run transaction within post_save” and I was led to this entry and eventually this entry