Django Signals

Django includes a “signal dispatcher” which helps allow decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events.

Django provides a set of built-in signals that let user code get notified by Django itself of certain actions. They may be useful when you’d like to take certain actions pre/post saving/deleting of objects. These include some useful notifications:

  • django.db.models.signals.pre_save & django.db.models.signals.post_save
  • django.db.models.signals.pre_delete & django.db.models.signals.post_delete

See the built-in signal documentation for a complete list, and a complete explanation of each signal.

Listening to signals
To receive a signal, you need to register a receiver function that gets called when the signal is sent by using the Signal.connect() method:

Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)

Receiver functions
First, we need to define a receiver function. A receiver can be any Python function or method:

def my_callback(sender, **kwargs):
    print("Request finished!")

Example of logging changes in Django application

Log changes on pre_save of object

def log_change_to_admin(sender, instance, **kwargs):
    '''
    Custom audit trail for any model.  This adds an entry into the django admin logs.  We'll get two entries per edit but at least we'll have the changes.
    Handles cases where the user adds or edits a model
    '''
    try:
        #if you want to override the log object do it as a _log_obj method on the instance, returning an object
        log_obj = instance._log_obj()
    except:
        log_obj = instance
    try:
        #if you want to override the excluded fields do it as a _log_excluded_fields method on the instance, returning a list
        excluded_fields = instance._log_excluded_fields()
    except:
        excluded_fields = []

    try:

        log_message = None

        #the "raw" check is to make sure we are not loading fixtures
        if not kwargs["raw"]:

            #if there is an id then we are in an edit situation
            if instance.id:
                #get the old object from the database
                old = sender.objects.get(pk=instance.id)

                #figure out what changed
                changed = []
                if old._meta and old._meta.fields:
                    for f in [field for field in old._meta.fields if field.name not in excluded_fields]:
                        if getattr(instance, f.name) != getattr(old, f.name):
                            old_value = "'%s'" % getattr(old, f.name)
                            new_value = "'%s'" % getattr(instance, f.name)
                            changed.append((f.name, old_value, new_value))

                #only log stuff if it has changed.
                if changed:
                    log_message = "Changes to %s: " % instance.__unicode__()
                    log_message += ', '.join(["%s from %s to %s" % (x[0], x[1], x[2]) for x in changed])
                    flag = CHANGE

            #since there was no id then we are in an add situation
            else:
                log_message = "Added %s: %s" % (sender.__name__, instance.__dict__)
                flag = ADDITION

        if log_message:
            u = 1 #default to root user if the request object is not attached.

            #add to the django log
            LogEntry.objects.log_action(
                user_id = u,
                content_type_id = ContentType.objects.get_for_model(log_obj).id,  #content_type_id for the account object
                object_id = log_obj.id,
                object_repr = log_obj.__unicode__(),
                change_message = log_message.rstrip(),
                action_flag = flag,
                )
    except:
        print "Failed to log action."

#connect log_change_to_admin to the pre_save signal for Installations and associated children
models.signals.pre_save.connect(log_change_to_admin, sender=Installation)

Log changes on pre_delete of object

def log_delete_to_admin(sender, instance, **kwargs):
    '''
    Custom audit trail for models.  This adds an entry into the django admin logs.  We'll get two entries per edit but at least we'll have the changes.
    Handles cases where the user deletes a model
    '''
    try:
        #if you want to override the log object do it as a _log_obj method on the instance, returning an object
        log_obj = instance._log_obj()
    except:
        log_obj = instance

    try:

        log_message = "Deleting %s: %s" % (instance.__unicode__(), instance.__dict__)

        u = 1 #default to root user if the request object is not attached.

        #add to the django log
        LogEntry.objects.log_action(
            user_id = u,
            content_type_id = ContentType.objects.get_for_model(log_obj).id,  #content_type_id for the account object
            object_id = log_obj.id,
            object_repr = log_obj.__unicode__(),
            change_message = log_message.rstrip(),
            action_flag = DELETION,
            )
    except:
        print "Failed to log action."

#connect log_delete_to_admin to the pre_delete signal for Installations and associated children
models.signals.pre_delete.connect(log_delete_to_admin, sender=Installation)

You can define your own custom rules and attach them to signals. e.g. I wanted to conditionally do some synchronization with SFDC after the object was saved. I created a post_save signal and hooked it up with a receiver that did the job.

# receiver to work on the SFDC changes for incoming request
def syncAccountToSfdc(sender, instance, **kwargs):
    if instance.isCustomer() and instance.account.salesforce_type != instance.account.getComputedSfdcAccountType() \
    and instance.account.salesforce_type in [SfdcAccountType.CUSTOMER, SfdcAccountType.CUSTOMER_IN_DANGER, SfdcAccountType.FORMER_CUSTOMER]:
        instance.account.update_salesforce()

# post_save signal to sync to SFDC if required
models.signals.post_save.connect(syncAccountToSfdc, sender=Installation)
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s