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).