I have these two basic models,
class Product(models.Model):
title = models.CharField(max_length=40)
description = models.TextField(blank=True)
price = models.DecimalField(decimal_places=2, max_digits=7, default=0)
...
and
class Cart(models.Model):
user = models.ForeignKey(
User, null=True, blank=True, on_delete=models.CASCADE)
products = models.ManyToManyField(
Product, blank=True, through='CartItem')
total = models.DecimalField(default=0.00, max_digits=7, decimal_places=2)
def recalculate_and_save(self):
print("recalculate_and_save running")
total = 0
for ci in self.cartitem_set.all():
total = ci.product.price*ci.quantity
self.total = total
self.save()
, and a helper model for many-to-many relation above, to account for quantity:
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.SmallIntegerField(default=1)
What I want to achieve is automatic calculation of Cart's total every time an item is added or removed. So,
@receiver(m2m_changed, sender=CartItem)
def m2m_changed_cart_receiver(sender, instance, action, *args, **kwargs):
print(f"m2m_changed received; action={action}, instance={instance}")
instance.recalculate_and_save()
Then I realized, seeing pre_save and post_save actions in logs, that I'm likely doing something wrong - calling save in the function which is called (twice) from parent's save, as per docs. The first question is, then - why doesn't it send me into an infinite loop? And the second (and probably more important) - why am I only seeing the receiver function executing on removing items from the cart, but not on adding them? Removing is done via
cart.products.remove(product_obj)
, but adding via
cart_obj.cartitem_set.add(cart_item_obj, bulk=False)
, which is probably related to why removal triggers the receiver, while adding does not. But that complicates the question even more - having sender set to be CartItem, I would expect removal, performed on product, to miss the receiver, not adding, which works on cartitems directly (though removal of products removes CartItems too, through the on_delete=CASCADE).
CodePudding user response:
The m2m_changed
signal will be triggered when modifying a ManyToManyField
(products
in this case).
From the docs
:
Sent when a ManyToManyField is changed on a model instance
So for your questions:
why doesn't it send me into an infinite loop?
This is because recalculate_and_save
didn't do anything with the m2m field products
, so it won't trigger the m2m_changed
signal again avoiding the infinite loop.
why am I only seeing the receiver function executing on removing items from the cart, but not on adding them?
Since the signal will only come from when modifying products
, doing things with cartitem_set
will not trigger it.
So you need to manually call recalculate_and_save
after modifying cartitem_set
:
cart_obj.cartitem_set.add(cart_item_obj, bulk=False)
cart_obj.recalculate_and_save()