django post_save signals on update - python

I am trying to set up some post_save receivers similar to the following:
#receiver(post_save, sender=Game, dispatch_uid='game_updated')
def game_updated(sender, **kwargs):
'''DO SOME STUFF HERE'''
MyPick.objects.filter(week=game.week, team=game.home_team).update(result=home_result)
MyPick.objects.filter(week=game.week, team=game.away_team).update(result=away_result)
#receiver(post_save, sender=MyPick, dispatch_uid='user_pick_updated')
def update_standings(sender, **kwargs):
'''DO STUFF'''
The first receiver is getting called correctly after an update on the Game object, however the calls to update on the MyPick object are not causing the second receiver to be called. Does the post_save signal not work on update or am I missing something else here?

update() is converted directly to an SQL statement; it doesn't call save() on the model instances, and so the pre_save and post_save signals aren't emitted. If you want your signal receivers to be called, you should loop over the queryset, and for each model instance, make your changes and call save() yourself.

Just one more thing to #Ismali Badawi's answer.
This calls post_save
user = User.objects.get(id=1)
user.username='edited_username'
user.save()
This does not call post_save
User.objects.filter(id=1).update(username='edited_username')
In the code,
from django.db.models.signals import post_save
#receiver(post_save, sender=User)
def do_something_when_user_updated(sender, instance, created, **kwargs):
if not created:
# User object updated
user_obj = instance
pass

Related

Debug Django signal handlers

I am trying to use signals for the first time. I have a model, UserAlias, and I want to execute some code after a UserAlias record is created.
UserAlias is defined in aliases/models.py
I created a aliases/signals/handlers.py. Here is that file's contents:
from django.db.models.signals import post_save, post_init, post_delete
from django.dispatch import receiver
from aliases.models import UserAlias
#receiver(post_init, sender=UserAlias)
def post_init_handler(sender, **kwargs):
print('hello there from a signal')
#receiver(post_save, sender=UserAlias)
def post_save_handler(sender, **kwargs):
print('hello there from a signal')
#receiver(post_delete, sender=UserAlias)
def post_delete_handler(sender, **kwargs):
print('hello there from a signal')
But when I execute:
from aliases.models import *
newalias = UserAlias.objects.create(...omitted...)
I do not see any of my debug print statements execute.
What am I missing here?
UPDATED:
I moved the definitions of my post_init_handler, post_save_handler and post_delete_handler to my aliases/models.py file after the declaration of my UserAlias class.
Also my post_init_handler now looks like this:
#receiver(post_init, sender=UserAlias)
def post_init_handler(sender, instance, **kwargs):
print(f'hello there from a signal {sender} {instance}')
I tried declaring it as Paaksing suggested ...
#receiver(post_init, sender=UserAlias)
def post_init_handler(sender, instance, created, **kwargs):
print(f'hello there from a signal {created} {sender} {instance}')
But I would get this error:
TypeError: post_init_handler() missing 1 required positional argument: 'created'
UPDATE II:
The post_init handler does not have a created parameter. The created parameter goes with the post_save handlers. Like this:
#receiver(post_save, sender=UserAlias)
def post_save_handler(sender, instance, created, **kwargs):
if created:
print(f'hello from post_save_handler( {sender}, {instance}, {created})')
You missed a created attr.
Consider adding instance if working with models that are user-like so you can manipulate them on the signals.
#receiver(post_save, sender=UserAlias)
def post_save_handler(sender, instance, created, **kwargs):
if created:
print('hello there from a signal')
Update: As Red Cricket said, the post_init does not have a created instance, but works with post_save as expected.
In the django doc shows all the instances that you can work with each signal: https://docs.djangoproject.com/en/3.0/ref/signals/
You created your signals in a seperate module (which is good). You also need to import the module at the right direction to use them.
The Django documentation advices to put the signals inside your app config file.
You could do it for example like this.
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myproject.app'
def ready(self) -> None:
import myproject.app.aliases/signals/handlers # noqa

Django - how to get all models bound to post_save receivers?

I'm working on a script which finds all Django pre_save and post_save signals and comments overwritten save methods so programmer is aware of all signals affecting the workflow.
For Example
There are two receivers:
#receiver(pre_save, sender=MyModel)
def sig_mymodel_pre_save(instance, sender, **kwargs):
...
#receiver(post_save, sender=MyModel)
def sig_mymodel_post_save(instance, sender, created, **kwargs):
...
And model MyModel:
class MyModel(..):
...
def save(self,*args,**kwargs):
...
super().save(...)
....
I want the script to modify MyModel code to look like this:
class MyModel(..):
...
def save(self,*args,**kwargs):
...
# SIGNAL pre_save | myapp.models.sig_mymodel_pre_save
super().save(...)
# SIGNAL post_save | myapp.models.sig_mymodel_post_save
....
So the first thing I'm going to do is to list all receivers of post_save and pre_save signals:
def get_signals() -> {}:
result = {}
for signal in [pre_save, post_save]:
result[signal] = signal.receivers
return result
def comment_signals_to_save_methods():
for signal, receivers in get_receivers():
for receiver in receivers:
models = ???
And here is the problem - I can't get models for the receiver. There is no such method or attribute.
Do you know how to do it?

Django - Pass value from pre_save to post_save methods on model

I've got a Django model like so...
class Example(models.Model):
title = models.CharField(...)
...
I'm trying to compare two values - the title field before the user changes it, and the title after. I don't want to save both values in the database at one time (only need one title field), so I'd like to use pre_save and post_save methods to do this. Is it possible to get the title before the save, then hold this value to be passed into the post_save method?
The pre_save and post_save methods look like so...
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the current title name here
x = instance.title
#receiver(post_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the new title name here and compare the difference
x = instance.title # <- new title
if x == old_title_name: # <- this is currently undefined, but should be retrieved from the pre_save method somehow
...do some logic here...
Any ideas would be greatly appreciated!
Edit
As was pointed out to me, pre_save and post_save both occur after save() is called. What I was looking for is something like pre_save() but before the actual save method is called. I set this on the model so that the logic to be performed will be accessible wherever the instance is saved from (either admin or from a user view)
Use Example.objects.get(pk=instance.id) to get the old title from the database in the pre_save handler function:
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
new_title = instance.title # this is the updated value
old_title = Example.objects.get(pk=instance.id)
# Compare the old and new titles here ...
This trick was proposed here a long time ago. I've not tested it with the recent Django version. Please let me know whether it's still working.
We can only say object has changed if "save" method passes successfully, so post_save is good to be sure that model object has updated.
Setting on the fly attribute on model class instance can do the task, as the same instance is passed from pre_save to post_save.
def set_flag_on_pre_save(sender, instance, *args, **kwargs):
# Check here if flag setting condition satisfies
set_the_flag = true
if set_the_flag:
instance.my_flag=0
def check_flag_on_post_save(sender, instance, *args, **kwargs):
try:
print(instance.my_flag)
print('Flag set')
except AttributeError:
print('Flag not set')
pre_save.connect(set_flag_on_pre_save, sender=ModelClass)
post_save.connect(check_flag_on_post_save, sender=ModelClass)

Identify the changed fields in django post_save signal

I'm using django's post_save signal to execute some statements after saving the model.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# do some stuff
pass
Now I want to execute a statement based on whether the value of the mode field has changed or not.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# if value of `mode` has changed:
# then do this
# else:
# do that
pass
I looked at a few SOF threads and a blog but couldn't find a solution to this. All of them were trying to use the pre_save method or form which are not my use case. https://docs.djangoproject.com/es/1.9/ref/signals/#post-save in the django docs doesn't mention a direct way to do this.
An answer in the link below looks promising but I don't know how to use it. I'm not sure if the latest django version supports it or not, because I used ipdb to debug this and found that the instance variable has no attribute has_changed as mentioned in the below answer.
Django: When saving, how can you check if a field has changed?
If you want to compare state before and after save action, you can use pre_save signal which provide you instance as it should become after database update and in pre_save you can read current state of instance in database and perform some actions based on difference.
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=MyModel)
def on_change(sender, instance: MyModel, **kwargs):
if instance.id is None: # new object will be created
pass # write your code here
else:
previous = MyModel.objects.get(id=instance.id)
if previous.field_a != instance.field_a: # field will be updated
pass # write your code here
Ussually it's better to override the save method than using signals.
From Two scoops of django:
"Use signals as a last resort."
I agree with #scoopseven answer about caching the original value on the init, but overriding the save method if it's possible.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
__original_mode = None
def __init__(self, *args, **kwargs):
super(Mode, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.mode != self.__original_mode:
# then do this
else:
# do that
super(Mode, self).save(force_insert, force_update, *args, **kwargs)
self.__original_mode = self.mode
UPDATE IF YOU NEED SIGNALS
Just in case you really need signals because you need a decoupled app or you can't simply override the save() method, you can use pre_save signal to 'watch' previous fields
#receiver(pre_save, sender=Mode)
def check_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
if instance.mode != original_mode:
# then do this
else:
# do that
The problem with this is that you make changes before, so if save() has a problem you could have some issues later.
So to fix that issue, you can store the original value on the pre_save and use on post_save.
#receiver(pre_save, sender=Mode)
def cache_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
instance.__original_mode = original_mode:
#receiver(post_save, sender=Mode)
def post_save_mode_handler(sender, instance, created, **kwargs):
if instance.__original_mode != instance.original_mode:
# then do this
else:
# do that
The problem with signals and this approach also is that you need one more query to check previous values.
Set it up on the __init__ of your model so you'll have access to it.
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
Now you can perform something like:
if instance.mode != instance.__original_mode:
# do something useful
This is an old question but I've come across this situation recently and I accomplished it by doing the following:
class Mode(models.Model):
def save(self, *args, **kwargs):
if self.pk:
# If self.pk is not None then it's an update.
cls = self.__class__
old = cls.objects.get(pk=self.pk)
# This will get the current model state since super().save() isn't called yet.
new = self # This gets the newly instantiated Mode object with the new values.
changed_fields = []
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
changed_fields.append(field_name)
except Exception as ex: # Catch field does not exist exception
pass
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
This is more effective since it catches all updates/saves from apps and django-admin.
in post_save method you have kwargs argument that is a dictionary and hold some information. You have update_fields in kwargs that tell you what fields changed. This fields stored as forzenset object. You can check what fields changed like this:
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
if not created:
for item in iter(kwargs.get('update_fields')):
if item == 'field_name' and instance.field_name == "some_value":
# do something here
But there is an issue in this solution. If your field value for example was 10, and you update this field with 10 again, this field will be in update_fields again.
I'm late but it can be helpful for others.
We can make custom signal for this.
Using custom signal we can easily do these kind of things:
Post is created or not
Post is modified or not
Post is saved but any field does not changed
class Post(models.Model):
# some fields
Custom signals
**Make signal with arguments **
from django.dispatch import Signal, receiver
# provide arguments for your call back function
post_signal = Signal(providing_args=['sender','instance','change','updatedfields'])
Register signal with call back function
# register your signal with receiver decorator
#receiver(post_signal)
def post_signalReciever(sender,**kwargs):
print(kwargs['updatedfields'])
print(kwargs['change'])
Sending the signal from post-admin
We sending the signals from Post admin and also save object when it actually modified
#sending the signals
class PostAdmin(admin.ModelAdmin):
# filters or fields goes here
#save method
def save_model(self, request, obj, form, change):
if not change and form.has_changed(): # new post created
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post created')
elif change and form.has_changed(): # post is actually modified )
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post modified')
elif change and not form.has_changed() :
print('Post not created or not updated only saved ')
See also:
Django Signals official doc
This can be identified using instance._state.adding
if not instance._state.adding:
# update to existing record
do smthng
else:
# new object insert operation
do smthng
You can use update_fields in django signals.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# only update instance
if not created:
update_fields = kwargs.get('update_fields') or set()
# value of `mode` has changed:
if 'mode' in update_fields:
# then do this
pass
else:
# do that
pass

Several Django signal receivers in one module

I have separate python module for signal receivers, it's called signals.py and imported in ready() method of my AppConfig.
In this module I implemented post_save and post_delete signal receivers for specific model and registered them via decorator:
#receiver(post_save, sender=MyModel)
def generate_smth(sender, instance, created, **kwargs):
...
And it works fine.
But, when I added to signals.py receivers of same signals in the same manner, but from different specific models:
#receiver(post_save, sender=AnotherModel)
def generate_smth(sender, instance, created, **kwargs):
...
My functions stopped to receive signals. But if I move receivers into separate python modules mymodel_signals.py and anothermodel_signals.py and import both modules in ready() then all of them works again.
Why it isn't possible to keep receivers in one module?
#receiver(post_save, sender=MyModel)
#receiver(post_save, sender=AnotherModel)
def generate_smth(sender, instance, created, **kwargs):
if sender.__name__ = 'MyModel':
# Bar
else:
# Foo
Do you want both functions to have the same behaviour? If yes, you can do:
def do_smth(sender, instance, created, **kwargs):
...
#receiver(post_save, sender=MyModel)
def generate_smth(sender, instance, created, **kwargs):
do_smth(sender, instance, created, **kwargs)
#receiver(post_save, sender=AnotherModel)
def generate_another_smth(sender, instance, created, **kwargs):
do_smth(sender, instance, created, **kwargs)

Categories

Resources