Сигнал m2m_changed со сквозным прохождением "многие-ко-многим" - сохранение

У меня есть две основные модели,

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

и

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

, и вспомогательная модель для отношения "многие ко многим" выше, для учета количества:

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)

Я хочу добиться автоматического вычисления общей суммы корзины каждый раз, когда товар добавляется или удаляется. Итак,

@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()

Потом, увидев в логах действия pre_save и post_save, я понял, что, скорее всего, делаю что-то не так - вызываю save в функции, которая вызывается (дважды) из родительского save, согласно документации. Тогда первый вопрос - почему это не отправляет меня в бесконечный цикл? И второй (и, вероятно, более важный) - почему я вижу, что функция-приемник выполняется только при удалении товаров из корзины, но не при их добавлении? Удаление происходит через

cart.products.remove(product_obj)

, но добавление via

cart_obj.cartitem_set.add(cart_item_obj, bulk=False)

, что, вероятно, связано с тем, почему удаление срабатывает на получателя, а добавление - нет. Но это еще больше усложняет вопрос - имея отправителем CartItem, я бы ожидал, что удаление, выполняемое для продукта, пропустит получателя, а не добавление, которое работает непосредственно с cartitems (хотя удаление продуктов удаляет и CartItems, через on_delete=CASCADE).

При модификации m2m_changed (в данном случае ManyToManyField) срабатывает сигнал products.

От docs:

Sent when a ManyToManyField is changed on a model instance

Итак, на ваши вопросы:


почему он не отправляет меня в бесконечный цикл?

Это происходит потому, что recalculate_and_save ничего не сделал с полем m2m products, поэтому он не вызовет сигнал m2m_changed снова, избегая бесконечного цикла.


почему я вижу выполнение функции приемника только при удалении товаров из корзины, но не при их добавлении?

Поскольку сигнал будет поступать только при модификации products, действия с cartitem_set не вызовут его.

Поэтому вам нужно вручную вызвать recalculate_and_save после модификации cartitem_set:

cart_obj.cartitem_set.add(cart_item_obj, bulk=False)
cart_obj.recalculate_and_save()
Вернуться на верх