how to signals   0   12
Using signals in Django to log changes in models

Sometimes we need to know who made what changes to which table. This might be required for legal audit purpose or for simple organisational level logging.

There are multiple Django apps available online which can help you log the model changes but there is no fun in doing that.

We will see how to do it without using ready-made app and hence will learn something in the process.



Signals:

Signals lets a sender notify another receiver that some event have occurred and some action needs to be performed.

For example, we have some data in cache as well in DB. We read data from cache and if not found then goes to DB as fallback. Now whenever a DB is updated, we need to update the cache as well.

But we might update the model from multiple views. Hence it is tough and not clean to write cache update logic in every such view.

Signals comes into picture now. Signal system have two main components. Senders and Receivers.

As name suggests, sender dispatches the signals and receiver receives them to perform some action.  To receive a signals, we need to register the receiver function. Every time a model is updates, signal is dispatched and required action is performed by receiver.

Django have a set of built-in signals which sends notification to user code. You can read all about signals in official Django documentation.

For this article, we will require, pre_save, post_save and pre_delete signals which are  part of  a set of signals sent by Django models.  django.db.models.signals module defines this set of signals.



Creating Signal:

To log any changes made to some model, we need to use either pre_save  or post_save  signal. We will keep the things simple and then will proceed to specific scenarios slowly.

So for now lets say we need to log changes made to every model.

First create a file signals.py in your app's root directory.

Create a function save_model_changes in signals file. This function will work as receiver.

from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver


# this receiver is executed every-time some data is saved in any table
@receiver(pre_save)
def audit_log(sender, instance, **kwargs):
    # code to execute before every model save
    print("Inside signal code")


To execute the code in receiver function for just one model, define the sender in @receiver decorator.

@receiver(pre_save, sender=MyModel)
   

Before we proceed further, we need to tell out app config about the signal we just created. Inside your app.py file, import the signals.

from django.apps import AppConfig


class MyAppConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        # everytime server restarts
        import myapp.signals

Inside your app's __init__.py file,

default_app_config = 'myapp.apps.MyAppConfig'


Now our signal is ready. Every-time we save anything to any model, we can see "Inside signal code" being printed in terminal.  


Logging model Changes:

Now our signal is ready and working, all we need to do is to write the logic to log the changes in audit table. Create a table where we will be storing the changes.

Table structure may vary as per your application's requirement. My audit table looks something like below.

from django.db import models


class ModelChangeLogsModel(models.Model):
    user_id = models.BigIntegerField(null=False, blank=True, db_index=True) 
    table_name = models.CharField(max_length=132, null=False, blank=True)
    table_row = models.BigIntegerField(null=False, blank=True)
    data = models.TextField(null=False, blank=True)
    action = models.CharField(max_length=16, null=False, blank=True)  # saved or deleted
    timestamp = models.DateTimeField(null=False, blank=True)

    class Meta:
        app_label = "myapp"
        db_table = "model_change_logs"


So I will be storing, which user made the changes to which table and what row number was edited.

Data will contain the snapshot of row in JSON format. Action column will  be used to store whether row was updated or deleted.

Now simplest thing will be to store the complete serialised model instance in data column, but that will take a lot of space. You may store only the fields which were changed and can ignore the rest. For this, we need to hit the database once more to retrieve the previous instance of model and compare it with current instance and then save the changed fields.


Getting previous instance of model:

    try:
        table_pk = instance._meta.pk.name
        table_pk_value = instance.__dict__[table_pk]
        query_kwargs = dict()
        query_kwargs[table_pk] = table_pk_value
        prev_instance = sender.objects.get(**query_kwargs) # for dynamic column name
    except ObjectDoesNotExist as e:
        # this instance is being created and not updated. ignore and return
        logging.getLogger("info_logger").info("Signals: creating new instance of "+str(sender))
        return


We first get the primary key of instance and try to fetch the older instance of model from database.

But if we are not updating the instance, instead we are creating it for first time, we will not find any older instance in DB hence we need to handle it , which is done via try except block above.

You can get the table name as str(instance._meta.db_table)

So this was the basics of how to use signals to log the model changes. You can optimise the approach as per your requirement, like tracking only specific/critical models like payments and finances or storing only few specific columns.


how to signals   0   12

0 thoughts on 'Using Signals In Django To Log Changes In Models'
Leave a comment:


*All Fields are mandatory. **Email Id will not be published publicly.


SUBSCRIBE
Please subscribe to get the latest articles in your mailbox.



Recent Posts: