92

I have a question about django.

I have ManyToMany Models here

class Product(models.Model):
     name = models.CharField(max_length=255)
     price = models.DecimalField(default=0.0, max_digits=9, decimal_places=2)
     stock = models.IntegerField(default=0)

     def  __unicode__(self):
         return self.name

class Cart(models.Model):
    customer = models.ForeignKey(Customer)
    products = models.ManyToManyField(Product, through='TransactionDetail')
    t_date = models.DateField(default=datetime.now())
    t_sum = models.FloatField(default=0.0)

    def __unicode__(self):
         return str(self.id)

class TransactionDetail(models.Model):
    product = models.ForeignKey(Product)
    cart = models.ForeignKey(Cart)
    amount = models.IntegerField(default=0)

For 1 cart object created, I can insert as many as new TransactionDetail object (the product and amount). My question is. How can I implement the trigger? What I want is whenever a Transaction detail is created, I want the amount of the product's stock is substracted by the amount in the transactiondetail.

I've read about post_save() but I'm not sure how to implement it. maybe something like this

when:

post_save(TransactionDetail, 
       Cart) #Cart object where TransactionDetail.cart= Cart.id
Cart.stock -= TransactionDetail.amount
hjpotter92
  • 78,589
  • 36
  • 144
  • 183
haris hamdani
  • 1,367
  • 2
  • 11
  • 18

5 Answers5

203

If you really want to use signals to achieve this, here's briefly how,

from django.db.models.signals import post_save
from django.dispatch import receiver

class TransactionDetail(models.Model):
    product = models.ForeignKey(Product)

# method for updating
@receiver(post_save, sender=TransactionDetail, dispatch_uid="update_stock_count")
def update_stock(sender, instance, **kwargs):
    instance.product.stock -= instance.amount
    instance.product.save()
Ben J.
  • 766
  • 1
  • 6
  • 17
Kenny Shen
  • 4,773
  • 3
  • 21
  • 18
  • 1
    THis is working fine for me but do not know why it is in loop of unknown length – Manoj Sahu Dec 12 '15 at 13:19
  • 8
    I am getting `maximum recursion depth exceeded` error, because I was saving the instance itself in the `@receiver` function. How can I achieve to update self models ? Do I have to override `save()` method of models? – Dipak Mar 15 '16 at 06:56
  • 2
    @Dipak maximum recursion depth exceeded because every time you update the instance the post_save is triggered so it is calling itself on every save, at a state the recursion depth value exceeds the exception arises. let me know how did you overcome that – Vamsidhar Muggulla Dec 27 '16 at 10:14
  • 3
    @VamsidharMuggulla Instead of using `signal` I override `save` method of model and using `updated` function updated model property, so that it wont trigger save again. – Dipak Dec 27 '16 at 11:17
  • @Dipak yeah exactly what I expected. – Vamsidhar Muggulla Dec 27 '16 at 11:39
  • 14
    What is `dispatch_uid` for? – R11G May 09 '18 at 23:25
  • @R11G - https://docs.djangoproject.com/en/2.1/topics/signals/#preventing-duplicate-signals – Vishal Nagda Aug 09 '18 at 13:16
  • I'm using django 3.2.4, post_save signal is working while debug is true both in local and production. any idea to solve this issue? More details about my question: https://stackoverflow.com/questions/69620161/django-signal-post-save-is-not-working-while-debug-is-false – Mehady Oct 19 '21 at 05:59
  • does the receiver and model should be in same file? – Ajay Gidwani Jun 14 '23 at 13:08
22

Personally I would override the TransactionDetail's save() method and in there save the new TransactionDetail and then run

self.product.stock -= self.amount
self.product.save()
Mikael
  • 3,148
  • 22
  • 20
19

If you want to avoid getting maximum recursion depth exceeded, then you should disconnect signals, before saving within the signal handler. The example above (Kenny Shen's answer), would then be:

from django.db.models.signals import post_save
from django.dispatch import receiver

class TransactionDetail(models.Model):
    # ... fields here

# method for updating
@receiver(post_save, sender=TransactionDetail, dispatch_uid="update_stock_count")
def update_stock(sender, instance, **kwargs):
 instance.product.stock -= instance.amount

 post_save.disconnect(update_stock, sender=TransactionDetail)
 instance.product.save()
 post_save.connect(update_stock, sender=TransactionDetail)

This is described thoroughly in Disconnect signals for models and reconnect in django, with a more abstract and useful example.

Also see: https://docs.djangoproject.com/en/2.0/topics/signals/#disconnecting-signals in the django docs.

  • 2
    in order for this to work, you need to have `dispatch_uid` in the `disconnect` and `connect` methods. The return values help you deal with potential errors. ` is_disconnected = post_save.disconnect(update_stock, sender=TransactionDetail, dispatch_uid="update_stock_count") if is_diconnected: instance.product.save() is_reconnected = post_save.connect(update_stock, sender=TransactionDetail, dispatch_uid="update_stock_count") ` – Califlower Jan 10 '19 at 01:46
  • Do not do this. Disconnecting signals could lead to concurrency issues. Especially in a financial application implied by the given code sample! What if two `TransactionDetail` objects are saved near the same time as each other? If the the first disconnects the signal, as the save() on the second is fired, the signal will not be received as it is disconnected! The correct way to handle this is to use queryset.update() which lets you "save" an object instance without calling save(). See also: https://stackoverflow.com/a/45895528/4028977 – Rob Jan 19 '22 at 07:28
6

If you really want to use signals in django please try this:

#import inbuilt user model
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_profile(sender, **kwargs):
    # write you functionality
    pass
   

then add default_app_config in the init file

 default_app_config = "give your AppConfig path"
Montaro
  • 9,240
  • 6
  • 29
  • 30
Ankit Patidar
  • 467
  • 5
  • 6
3

In fact, the docstring explains the Signals is in django.dispatch.Signal.connect:

def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
    Connect receiver to sender for signal.

    Arguments:

        receiver
            A function or an instance method which is to receive signals.
            Receivers must be hashable objects.

            If weak is True, then receiver must be weak referenceable.

            Receivers must be able to accept keyword arguments.

            If a receiver is connected with a dispatch_uid argument, it
            will not be added if another receiver was already connected
            with that dispatch_uid.

        sender
            The sender to which the receiver should respond. Must either be
            a Python object, or None to receive events from any sender.

        weak
            Whether to use weak references to the receiver. By default, the
            module will attempt to use weak references to the receiver
            objects. If this parameter is false, then strong references will
            be used.

        dispatch_uid
            An identifier used to uniquely identify a particular instance of
            a receiver. This will usually be a string, though it may be
            anything hashable.
Zulu
  • 8,765
  • 9
  • 49
  • 56