Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django: recursion using post-save signal

Here's the situation:

Let's say I have a model A in django. When I'm saving an object (of class A) I need to save it's fields into all other objects of this class. I mean I need every other A object to be copy of lat saved one.

When I use signals (post-save for example) I get a recursion (objects try to save each other I guess) and my python dies.

I men I expected that using .save() method on the same class in pre/post-save signal would cause a recursion but just don't know how to avoid it.

What do we do?

like image 271
grucha Avatar asked Jul 14 '10 13:07

grucha


3 Answers

@ShawnFumo Disconnecting a signal is dangerous if the same model is saved elsewhere at the same time, don't do that !

@Aram Dulyan, your solution works but prevent you from using signals which are so powerful !

If you want to avoid recursion and keep using signals (), a simple way to go is to set an attribute on the current instance to prevent upcoming signals firing.

This can be done using a simple decorator that checks if the given instance has the 'skip_signal' attribute, and if so prevents the method from being called:

from functools import wraps

def skip_signal():
    def _skip_signal(signal_func):
        @wraps(signal_func)
        def _decorator(sender, instance, **kwargs):
            if hasattr(instance, 'skip_signal'):
                return None
            return signal_func(sender, instance, **kwargs)  
        return _decorator
    return _skip_signal

We can now use it this way:

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

@receiver(post_save, sender=MyModel)
@skip_signal()
def my_model_post_save(sender, instance, **kwargs):
    # you processing
    pass

m = MyModel()
# Here we flag the instance with 'skip_signal'
# and my_model_post_save won't be called
# thanks to our decorator, avoiding any signal recursion
m.skip_signal  = True
m.save()

Hope This helps.

like image 59
Charlesthk Avatar answered Nov 06 '22 14:11

Charlesthk


This will work:

class YourModel(models.Model):
    name = models.CharField(max_length=50)

    def save_dupe(self):
        super(YourModel, self).save()

    def save(self, *args, **kwargs):
        super(YourModel, self).save(*args, **kwargs)
        for model in YourModel.objects.exclude(pk=self.pk):
            model.name = self.name
            # Repeat the above for all your other fields
            model.save_dupe()

If you have a lot of fields, you'll probably want to iterate over them when copying them to the other model. I'll leave that to you.

like image 43
Aram Dulyan Avatar answered Nov 06 '22 13:11

Aram Dulyan


Another way to handle this is to remove the listener while saving. So:

class Foo(models.Model):
  ...

def foo_post_save(instance):
  post_save.disconnect(foo_post_save, sender=Foo)
  do_stuff_toSaved_instance(instance)
  instance.save()
  post_save.connect(foo_post_save, sender=Foo)

post_save.connect(foo_post_save, sender=Foo)
like image 5
ShawnFumo Avatar answered Nov 06 '22 13:11

ShawnFumo