blog Django Object Updated Signal

Tags:
django

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.