Using Django Signals To Watch For Changes To Instances
Say you want to monitor changes to instances of a model and update something based on the changes. In my example I wanted to maintain a sum of the values that had certain characteristics. You can accomplish this with Django Signals.
Signals are events that fire at various pre-defined moments – for example, before an instance is saved, after it’s saved, etc. You can subscribe to these events, allowing your callback handler to be called at those moments.
The code below subscribes to the post_init and post_save signals. post_init gets triggered when a model’s __init__ class is done executing, which generally means when a model instance is created for the first time or instantiated from a query to the DB. This is actually too frequent for the use case I have in mind (checking the before-modification and after-modification values of certain fields), but seems to be the only place I can hook in to get the pre-modification values.
post_init gets triggered after the instance is saved to the DB. The code below stores the pre-modification values in pre_save when it gets triggered by the post_init signal, and checks them against the post-modification values when it gets triggered by the post_save signal.
Note that you’ll probably want to clean up pre_save periodically. Unfortunately post_init and post_save are not symmetrical (you’ll get a post_init anytime an instance is created, for example when you query the DB), so you can’t simply delete from pre_save when the post_save signal gets triggered.
from django.dispatch import dispatcher
from django.db.models import signals
pre_save = {}
def change_watcher(sender, instance, signal, *args, **kwargs):
print "SIGNAL:", sender, instance.report, signal, args, kwargs
if signal == signals.post_init:
pre_save[instance.id] = (instance.field1, instance.field2)
else:
if pre_save[instance.id][0] != instance.field1:
print "Changed field1"
if pre_save[instance.id][1] != instance.field2:
print "Changed field2"
for signal in (signals.post_init, signals.post_save):
dispatcher.connect(change_watcher, sender = Expense, signal = signal)
Manage your expenses via Email, SMS, iPhone, Twitter, Voice (Call and say your expense), IM (Yahoo, AIM, MSN), or Web.
Comments(8)
Why aren’t you using the pre-save signal instead of the post-init signal?
My understanding of pre-save and post-save is that they are in fact symmetric.
Dave, in my case I needed access to the pre-modification values, which are already over-written by the time pre-save happens. So if an instance has key1=value1 before the modification, and the update is key1=value2, I need access to value1 as well as value2. pre-save would only give me value2.
If you use the pre-save signal, you should be able to access the actual object in the db by using the id of the instance.
As an example, in your pre-save sginal, e = Expense.objects.get(pk=instance.id) should retrieve the unmodified object as it hasn’t been saved to the db yet
If you use the pre-save signal and reading from the db, aren’t you potentially getting someone else’s saved values?
bjorn, doesn’t this end up being the same way? There’s no guarantee that the pre_save dictionary will be accessed during post_save before someone else changes it with as post_init signal…
This is not thread safe.
thank you for the suggestion, but as bjorn said, i think too its better to do it via pre_save.
here is my solution:
def change_watcher(sender, instance, **kwargs):
# if the instance has no id, it is created
if instance.id:
old = Model.objects.get(id=instance.id)
print old.field, instance.field
pre_save.connect(change_watcher, Model)
Hello,
thanks for sharing this code. It helps me a lots.
I’m using Django 1.3 and the last line doesnt work with this version of Django.
I’ve changed
dispatcher.connect(change_watcher, sender = Expense, signal = signal)
to
signal.connect(change_watcher, sender = Expense)
It works like a charm.
Thanks again.