Django, post_save signal recrusion. How to bypass signal firing - django

I have a situation where when one of my models is saved MyModel I want to check a field, and trigger the same change in any other Model with the same some_key.
The code works fine, but its recursively calling the signals. As a result I am wasting CPU/DB/API calls. I basically want to bypass the signals during the .save(). Any suggestions?
class MyModel(models.Model):
#bah
some_field = #
some_key = #
#in package code __init__.py
#receiver(models_.post_save_for, sender=MyModel)
def my_model_post_processing(sender, **kwargs):
# do some unrelated logic...
logic = 'fun! '
#if something has changed... update any other field with the same id
cascade_update = MyModel.exclude(id=sender.id).filter(some_key=sender.some_key)
for c in cascade_update:
c.some_field = sender.some_field
c.save()

Disconnect the signal before calling save and then reconnect it afterwards:
post_save.disconnect(my_receiver_function, sender=MyModel)
instance.save()
post_save.connect(my_receiver_function, sender=MyModel)

Disconnecting a signal is not a DRY and consistent solution, such as using update() instead of save().
To bypass signal firing on your model, 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(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
Based on your example, that gives us:
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):
instance.some_field = my_value
# Here we flag the instance with 'skip_signal'
# and my_model_post_save won't be called again
# thanks to our decorator, avoiding any signal recursion
instance.skip_signal = True
instance.save()
Hope This helps.

A solution may be use update() method to bypass signal:
cascade_update = MyModel.exclude(
id=sender.id).filter(
some_key=sender.some_key).update(
some_field = sender.some_field )
"Be aware that the update() method is converted directly to an SQL statement. It is a bulk operation for direct updates. It doesn't run any save() methods on your models, or emit the pre_save or post_save signals"

You could move related objects update code into MyModel.save method. No playing with signal is needed then:
class MyModel(models.Model):
some_field = #
some_key = #
def save(self, *args, **kwargs):
super(MyModel, self).save(*args, **kwargs)
for c in MyModel.objects.exclude(id=self.id).filter(some_key=self.some_key):
c.some_field = self.some_field
c.save()

Related

Django: Update Record before save

Model:
class Tester:
test = models.ForeignKey(Platform)
count = models.Integer()
status = models.CharField(max_length=1, default='A')
I need to change the status to 'D' every time I insert a new record for the same test.
I tried using Signals pre_save, but that function went into a loop. I would really appreciate any help.
Signal fucntion probably goes into an infinite loop because you save the same model's instances in that function, in turn each one triggering the signal function itself. With a little care you can prevent this from happenning:
from django.db.models.signals import pre_save
#receiver(pre_save, sender=Tester)
def tester_pre_save(sender, instance, **kwargs):
if not instance.pk:
# This means that a new record is being created. We need this check as you want to do the operation when a new entry is **inserted** into table
Tester.objects.filter(test=instance.test).update(status='D')
or, with the post_save signal:
from django.db.models.signals import post_save
#receiver(post_save, sender=Tester)
def tester_post_save(sender, instance, created, **kwargs):
if created:
# This means that a new record has been created. We need this check as you want to do the operation when a new entry is **inserted** into table
Tester.objects.filter(test=instance.test).update(status='D')
Important points here is. as we are using update method of a query set to update existing entries, those won't trigger signals, because they don't use the model's save method, so that signal method won't be triggered for other instances we update here. And even if it were to be triggered, as we are doing the update operation under a condition check (if a new instance is being created), signal methods for those other saves wouldn't do anything, hence they would not cause an infinite loop.
Override the save method of our Tester class:
def save(self, *args, **kwargs):
if Tester.objects.filter(test=self.test).count()>0:
self.status="D"
else:
self.status="A"
super(Model, self).save(*args, **kwargs)
Put this into the class definition of Tester.

How to prevent recursion in django post_save signal?

I tried this answer https://stackoverflow.com/a/28369908/9902571 in my model and done the following :-
from functools import wraps
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
My model:
class Journal(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,null=True,blank=True)
company = models.ForeignKey(company,on_delete=models.CASCADE,null=True,blank=True,related_name='Companyname')
date = models.DateField(default=datetime.date.today)
voucher_id = models.PositiveIntegerField(blank=True,null=True)
by = models.ForeignKey(ledger1,on_delete=models.CASCADE,related_name='Debitledgers')
to = models.ForeignKey(ledger1,on_delete=models.CASCADE,related_name='Creditledgers')
debit = models.DecimalField(max_digits=10,decimal_places=2,null=True)
credit = models.DecimalField(max_digits=10,decimal_places=2,null=True)
My signal:
#receiver(post_save, sender=journal)
#prevent_recursion
def pl_journal(sender, instance, created, **kwargs):
if created:
if instance.by.group1_Name.group_Name == 'Indirect Expense':
Journal.objects.update_or_create(user=instance.user,company=instance.company,date=instance.date, voucher_id=instance.id, voucher_type= "Journal",by=instance.by,to=ledger1.objects.filter(user=instance.user,company=instance.company,name__icontains='Profit & Loss A/c').first(),debit=instance.debit,credit=instance.credit)
But the RecursionError of maximum recursion depth exceeded while calling a Python object stills comes while creating a journal object.
I want to pass the post_save signal for the same model (Journal) which when matches the condition in the signal gets called recursively.
Any anyone tell me what is wrong in my code?
Thank you
It is a little late but it may help others too. Instead of this, you can add dispatch_uid="identifier" to signal and it will work fine.
post_save.connect(function_name, sender=Model, dispatch_uid="identifier")
or
#receiver(post_save, sender=Model, dispatch_uid="identifier")
doc: https://docs.djangoproject.com/en/4.1/topics/signals/#preventing-duplicate-signals

Is save() method commiting changes asynchronously?

I have a simple class model with Django Admin (v. 1.9.2) like this:
from django.contrib.auth.models import User
class Foo(models.Model):
...
users = models.ManyToManyField(User)
bar = None
I have also overloaded save() method like this:
def save(self, *args, **kwargs):
self.bar = 1
async_method.delay(...)
super(Foo, self).save(*args, **kwargs)
Here async_method is an asynchronous call to a task that will run on Celery, which takes the users field and will add some values to it.
At the same time, whenever a user is added to the ManyToManyField, I want to do an action depending on the value of the bar field. For that, I have defined a m2m_changed signal:
def process_new_users(sender, instance, **kwargs):
if kwargs['action'] == 'post_add':
# Do some stuff
print instance.bar
m2m_changed.connect(process_new_users, sender=Foo.users.through)
And there's the problem. Although I'm changing the value of bar inside the save() method and before I call the asynchronous method, when the process_new_users() method is triggered, instance.bar is still None (initial value).
I'm not sure if this is because the save() method commits changes asynchronously and when the process_new_users() is triggered it has not yet commited changes and is retrieving the old value, or if I'm missing something else.
Is my assumption correct? If so, is there a way to force the values in save() be commited synchronously so I can then call the asynchronous method?
Note: Any alternative way of achieving this is also welcome.
UPDATE 1: As of #Gert's answer, I implemented a transaction.on_change() trigger so whenever the Foo instance is saved, I can safely call the asynchronous function afterwards. To do that I implemented this:
bar = BooleanField(default=False) # bar has became a BooleanField
def call_async(self):
async_method.delay(...)
def save(self, *args, **kwargs):
self.bar = True
super(Foo, self).save(*args, **kwargs)
transaction.on_commit(lambda: self.call_async())
Unfortunately, this changes nothing. Instead of None I'm now getting False when I should be getting True in the m2m_changed signal.
You want to make sure that your database is up to date. In Django 1.9, there is a new transaction.on_commit which can trigger celery tasks.

In Django, how do I avoid/skip/undo a pre-save event?

I am using django_extensions TimeStampedModel, which provides a modified field that sets itself via a pre_save event. Which is great, except I am converting an old schema and want to preserve the original modified datestamp. How can I monkeypatch, avoid, cancel, or replace the pre_save'd modified with another value?
In the end, I just did an end-around:
from django.db import connection
cursor = connection.cursor()
cursor.execute("update %s set modified='%s' where id=%s" % (
my_model._meta.db_table, desired_modified_date, my_model.id))
You cannot. Not in the sense you're asking.
What you can is create a fake field and populate it on clean().
Class MyModel(models.Model):
def clean(self):
self._modified = self.modified
...
#receiver(pre_save, sender=MyModel)
def receiver_(self, *args, **kwargs):
self.modified = self._modified
So you're backing up the field value and putting it back later. notes: ensure your application is loaded later.

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