Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django post_save preventing recursion without overriding model save()

There are many Stack Overflow posts about recursion using the post_save signal, to which the comments and answers are overwhelmingly: "why not override save()" or a save that is only fired upon created == True.

Well I believe there's a good case for not using save() - for example, I am adding a temporary application that handles order fulfillment data completely separate from our Order model.

The rest of the framework is blissfully unaware of the fulfillment application and using post_save hooks isolates all fulfillment related code from our Order model.

If we drop the fulfillment service, nothing about our core code has to change. We delete the fulfillment app, and that's it.

So, are there any decent methods to ensure the post_save signal doesn't fire the same handler twice?

like image 533
Yuji 'Tomita' Tomita Avatar asked May 31 '12 19:05

Yuji 'Tomita' Tomita


7 Answers

you can use update instead of save in the signal handler

queryset.filter(pk=instance.pk).update(....)
like image 70
mossplix Avatar answered Nov 12 '22 16:11

mossplix


Don't disconnect signals. If any new model of the same type is generated while the signal is disconnected the handler function won't be fired. Signals are global across Django and several requests can be running concurrently, making some fail while others run their post_save handler.

like image 40
punkgode Avatar answered Nov 12 '22 16:11

punkgode


What you think about this solution?

@receiver(post_save, sender=Article)
def generate_thumbnails(sender, instance=None, created=False, **kwargs):

    if not instance:
        return

    if hasattr(instance, '_dirty'):
        return

    do_something()

    try:
        instance._dirty = True
        instance.save()
    finally:
        del instance._dirty

You can also create decorator

def prevent_recursion(func):

    @wraps(func)
    def no_recursion(sender, instance=None, **kwargs):

        if not instance:
            return

        if hasattr(instance, '_dirty'):
            return

        func(sender, instance=instance, **kwargs)

        try:
            instance._dirty = True
            instance.save()
        finally:
            del instance._dirty

    return no_recursion


@receiver(post_save, sender=Article)
@prevent_recursion
def generate_thumbnails(sender, instance=None, created=False, **kwargs):

    do_something()
like image 28
xakdog Avatar answered Nov 12 '22 18:11

xakdog


I think creating a save_without_signals() method on the model is more explicit:

class MyModel()
    def __init__():
        # Call super here.
        self._disable_signals = False

    def save_without_signals(self):
        """
        This allows for updating the model from code running inside post_save()
        signals without going into an infinite loop:
        """
        self._disable_signals = True
        self.save()
        self._disable_signals = False

def my_model_post_save(sender, instance, *args, **kwargs):
    if not instance._disable_signals:
        # Execute the code here.
like image 27
Rune Kaagaard Avatar answered Nov 12 '22 18:11

Rune Kaagaard


How about disconnecting then reconnecting the signal within your post_save function:

def my_post_save_handler(sender, instance, **kwargs):
    post_save.disconnect(my_post_save_handler, sender=sender)
    instance.do_stuff()
    instance.save()
    post_save.connect(my_post_save_handler, sender=sender)
post_save.connect(my_post_save_handler, sender=Order)
like image 19
dgel Avatar answered Nov 12 '22 16:11

dgel


You should use queryset.update() instead of Model.save() but you need to take care of something else:

It's important to note that when you use it, if you want to use the new object you should get his object again, because it will not change the self object, for example:

>>> MyModel.objects.create(pk=1, text='')
>>> el = MyModel.objects.get(pk=1)
>>> queryset.filter(pk=1).update(text='Updated')
>>> print el.text
>>> ''

So, if you want to use the new object you should do again:

>>> MyModel.objects.create(pk=1, text='')
>>> el = MyModel.objects.get(pk=1)
>>> queryset.filter(pk=1).update(text='Updated')
>>> el = MyModel.objects.get(pk=1) # Do it again
>>> print el.text
>>> 'Updated'
like image 4
ruhanbidart Avatar answered Nov 12 '22 17:11

ruhanbidart


You could also check the raw argument in post_save and then call save_baseinstead of save.

like image 4
dragoon Avatar answered Nov 12 '22 17:11

dragoon