Reputation: 8722
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
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