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 🙂
[python]
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")
[/python]
You see, this function as supposed to be called by
[python]post_save.connect(set_m2m_contact_usergroup, sender=Contact) [/python]
when the Contact object was saved. See (part of) the model class below
[python]
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
[/python]
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:
[python]
def set_m2m_contact_usergroup(sender, instance, created, **kwargs):
if created:
transaction.on_commit(
lambda: instance.cou_group.add(instance.kit_user.cou_group)
)
[/python]
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