darkhorse
darkhorse

Reputation: 8722

How to make the m2m_changed signal atomic in Django?

I have the following models in my project:

class Topping(models.Model):
    name = models.CharField(max_length=255)

class Pizza(models.Model):
    name = models.CharField(max_length=255)
    toppings = models.ManyToManyField(Topping, blank=True)

    def save(self, *args, **kwargs):
        print('Saving...')
        if self.pk:
            for topping in self.toppings.all():
                print(topping.name)

        super(Pizza, self).save(*args, **kwargs)

def toppings_changed(sender, instance, **kwargs):
    instance.save()

m2m_changed.connect(toppings_changed, sender=Pizza.toppings.through)

Basically, whenever toppings is changed, the signal is fired. All the signal does is call the Pizza object's save method. Anyway, lets say I have three objects:

pizza = Pizza.objects.get(pk=1) # Number of toppings is 0
topping1 = Topping.objects.get(pk=1)
topping2 = Topping.objects.get(pk=2)

Now, I want to set the two toppings to my pizza. I do this using the following code:

pizza = Pizza.objects.get(pk=1)
pizza.toppings.set([1, 2])

The toppings are set correctly, however, the pizza's save method is called twice, because the m2m_changed signal is called twice, as there are two changes happening. How can I call this only once after all of the changes has been committed? To clarify, I want both of the toppings to be added, but I want to fire my signal only once, at the end of all the changes. Thanks for any help.

Upvotes: 2

Views: 1388

Answers (1)

ahmedaljawahiry
ahmedaljawahiry

Reputation: 136

The m2m_changed signal is slightly strange, in that it is called twice: once with an action of pre_add, and once with an action of post_add (see docs). Since you're calling toppings_changed() for any action, it will end up being called twice, hence save() will be called twice.

It looks like you're interested in post_add, so I would do something like:

@receiver(m2m_changed, sender=Pizza.toppings.through)
def toppings_changed(sender, instance, action, **kwargs):
    if action == "post_add":
        instance.save()

Here, instance.save() should only be called once, no matter how many arguments you're passing to set(). Note that you won't need to do m2m_changed.connect(...) if you use the receiver decorator.

The above also applies to pre/post_remove and pre/post_clear, so if you want to print your list after a removal, you'll need to add some more logic to check if action == "post_remove".

Finally, just in case you're unaware, set([1, 2]) will replace all of your toppings with the two you provided (pk=1 and pk=2). If you simply want to add toppings to your existing set, i'd use add(1, 2).

Hope this helps!

Upvotes: 7

Related Questions