Search code examples
pythondjangodjango-modelsdjango-signals

Django Signal Loop


Can someone help me resolving this problem. I've created an django-app with two models. One model is an Wallet model and other is Transaction model. Every transaction is conneted to a wallet with models.ForeignKey. I've also created two signals, one is for updating cryptocurrency balance (eg. BTC, ETH) in Wallet when transaction is made and the other signal i want to update Total_Balance (which is all other balances converted to USD). And here I have a problem because my signal is POST_SAVE and in it I have a save() method which causes infinity loop. I think if I would do a whole thing in my first signal it would work but in future i want to add new model which also will be connected to Wallet and it would pop-up my total_balance, therefor i would need same logic in third signal and I would end up with same code in two signals.

I know my description is a little messy. Here is some code to help understand it.

My Models:

class Wallet(models.Model):
    name = models.CharField(max_length=50)
    total_balance = models.IntegerField(blank=True)
    btc_balance = models.DecimalField(max_digits=15, decimal_places=8)
    xrp_balance = models.DecimalField(max_digits=15, decimal_places=8)
    eth_balance = models.DecimalField(max_digits=15, decimal_places=8)

class Transaction(models.Model):
    wallet = models.ForeignKey(Wallet, on_delete=models.CASCADE)
    currency_paid = models.CharField(choices=CURRENCY, max_length=3)
    amount_paid = models.DecimalField(max_digits=15, decimal_places=8)
    currency_recived = models.CharField(choices=CURRENCY, max_length=3)
    amount_recived = models.DecimalField(max_digits=15, decimal_places=8)

My Signals:

@receiver(post_save, sender=Transaction)
def create_transaction(sender, instance, created, **kwargs):
    if created:
        wallet = Wallet.objects.get(name = instance.wallet)

        currency_paid = instance.currency_paid
        currency_recived = instance.currency_recived
        amount_paid = instance.amount_paid
        amount_recived = instance.amount_recived

        # SUBSTRACK BALANCE
        if(currency_paid == 'BTC'):
            wallet.btc_balance -= amount_paid
            ...

        # ADDS BALANCE
        if(currency_recived == 'BTC'):
            wallet.btc_balance += amount_recived
            ...

        wallet.save()


@receiver(post_save, sender=Wallet)
def total_balance_update(sender, instance, created, **kwargs):
    if created == False:
        btc_price = cryptocompare.get_price('BTC',curr='USD')['BTC']['USD']
        xrp_price = cryptocompare.get_price('XRP',curr='USD')['XRP']['USD']
        ...


        btc_balance = float(instance.btc_balance)
        xrp_balance = float(instance.xrp_balance)
        ...
        
        total_balance = instance.total_balance

        total_balance = round(btc_balance * btc_price) + round(xrp_balance * xrp_balance) + round(eth_balance * eth_balance)

        instance.save()

Solution

  • The post_save of your Wallet performs an instance.save(), so that means that if you .save() your Wallet, it will trigger the post_save signal on the Wallet that will then again save the wallet, and thus each time the signal runs it saves the wallet again triggering the signal.

    As far as I can see, you do not need to use a post_save signal however, since you do not seem to use anything only available after the object has been saved. You can use a pre_save signal that will run before saving the object to the database:

    from django.db.models.signals import pre_save
    
    @receiver(pre_save, sender=Wallet)
    def total_balance_update(sender, instance, **kwargs):
        if instance.pk is not None:
            btc_price = cryptocompare.get_price('BTC',curr='USD')['BTC']['USD']
            xrp_price = cryptocompare.get_price('XRP',curr='USD')['XRP']['USD']
            ...
    
    
            btc_balance = float(instance.btc_balance)
            xrp_balance = float(instance.xrp_balance)
            ...
    
            instance.total_balance = round(btc_balance * btc_price) + round(xrp_balance * xrp_balance) + round(eth_balance * eth_balance)
            # no instance.save()

    We thus do not need to save the instance since immediately after running the signal, Django will start creating/updating the record at the database.