There have been a few times while working with Django , where I have wanted to triger something only if the instance changed. Since Django provides a robust signals system, I decided to use that for my prototype.
First we need to import the packages we will use.
from django.db import models
from django.db.models.signals import post_save, pre_save, ModelSignal
from django.dispatch import receiver
from django.forms.models import model_to_dict
Next, we want to handle our pre_save
signal.
We want to store a snapshot of the original object so that we can compare it later.
model_changes = ModelSignal()
changes_cache = {}
@receiver(pre_save)
def cache_pre_save(sender: models.Model, instance: models.Model, raw, **kwargs):
if instance.pk is None or raw is True:
return
changes_cache[instance] = model_to_dict(sender.objects.get(pk=instance.pk))
We create a cache to hold a snapshot of our object, and a new ModelSignal
for our changes signal.
If it is a raw instance (from loading a fixture) or a new object (that does not yet have a primary key) we will bail out.
Otherwise, we will do a database query to get a copy of the original object and save it to our cache.
Next, we want to handle post_save
where we check the difference of our object.
@receiver(post_save)
def check_post_save(sender, instance, raw, created, **kwargs):
if raw is True:
return
pre = changes_cache.pop(instance, None)
post = model_to_dict(instance)
if pre is None:
changes = {k: (None, post[k]) for k in post}
else:
changes = {k: (pre[k], post[k]) for k in post if pre[k] != post[k]}
model_changes.send(sender, instance=instance, created=created, changes=changes)
We again skip if it is a raw
instance from a loaded fixture.
We then take model_to_dict
of our post_save
instance and compare it.
If we have no pre_save
object then we know this is a new instance.
If not, we do a simple check of pre[k] != post[k]
to get our changes.
Lastly we send the result to our new signal.
@receiver(model_changes)
def print_changes(sender, **kwargs):
print("print_changes", sender, kwargs)
I am unsure how performant or useful this would be in a larger project, but at least this sketch seems like it would work.
There are several projects where I have wanted to see a diff of changes to objects, so using a signal like this, one could register model_changes
to save a copy (though would need to ensure that model_changes
never triggers on your DiffModel
type class if you do not want and endless loop.)
At the very least, writing this prototype scratched and itch and helped clear up a bit of storage in my brain. I may still package this up in a small Django app to wrap up the documentation of this idea.