Related
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?
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)
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
In the following sample code:
from django.db import models
from django.db.models.signals import pre_save
# Create your models here.
class Parent(models.Model):
name = models.CharField(max_length=64)
def save(self, **kwargs):
print "Parent save..."
super(Parent, self).save(**kwargs)
def pre_save_parent(**kwargs):
print "pre_save_parent"
pre_save.connect(pre_save_parent, Parent)
class Child(Parent):
color = models.CharField(max_length=64)
def save(self, **kwargs):
print "Child save..."
super(Child, self).save(**kwargs)
def pre_save_child(**kwargs):
print "pre_save_child"
pre_save.connect(pre_save_child, Child)
pre_save_parent doesn't fire when I a Child is created:
child = models.Child.objects.create(color="red")
Is this expected behaviour?
There's an open ticket about this, #9318.
Your workaround looks fine. Here are two others suggested on the ticket by benbest86 and alexr respectively.
Listen on the child class signal, and send the Parent signal there.
def call_parent_pre_save(sender, instance, created, **kwargs):
pre_save.send(sender=Parent, instance=Parent.objects.get(id=instance.id), created=created, **kwargs)
pre_save.connect(call_parent_pre_save, sender=Child)
Do not specify the sender when connecting the signal, then check for subclasses of parent.
def pre_save_parent(sender, **kwargs):
if not isinstance(instance, Parent):
return
#do normal signal stuff here
print "pre_save_parent"
pre_save.connect(pre_save_parent)
I didn't realise sender was an optional parameter to connect. I can do the following:
def pre_save_handler(**kwargs):
instance = kwargs['instance']
if hasattr(instance, 'pre_save'):
instance.pre_save()
pre_save.connect(pre_save_handler)
This allows me to write per Model pre_save methods, and they in turn can call any base class versions (if they exists).
Any better solutions?
I'm trying to register multiple signals on one model. It seems that as I register an additional signal, it removes the previous signal.
from django.dispatch import receiver
from django.db.models.signals import post_save,post_delete
from my.app.models import Resource
#receiver(post_save,sender=Resource)
def ResourceSaved(sender,**kwargs):
print "Saved"
#receiver(post_delete,sender=Resource)
def ResourceSaved(sender,**kwargs):
print "Deleted"
I've taken a look around the docs, but I keep finding details on how to create custom signals in a class formate. Not how to register in class format.
I would imagine I can do something like this:
#reciver(sender=Resource)
class SignalAnsweringMachine(object):
def post_delete(self,**kwargs):
print "delete"
def post_save(self,**kwargs):
print "save"
Thanks for your help in advance.
You are (probably unintentionally) redefining ResourceSaved. Try this instead:
#receiver(post_save,sender=Resource)
def ResourceSaved(sender,**kwargs):
print "Saved"
#receiver(post_delete,sender=Resource)
def ResourceDeleted(sender,**kwargs):
print "Deleted"
Since the merge of #18454 released in Django 1.5 you can connect more than one signal by supplying a list of signals:
#receiver([post_save, post_delete], sender=Resource)
def ResourceSaved(sender, **kwargs):
pass
I use this short form to register two or more handlers.
In my case I clear cached categories list.
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from ..models import Category
all_categories = []
# Post save handler for Category model to clear "all_categores" variable
#receiver(post_save, sender=Category)
#receiver(post_delete, sender=Category)
def post_save_category(sender, **kwargs):
# clean cached categories
global all_categories
all_categories = []