Django model signals

As you've learned throughout this chapter, Django models have a series of methods you can override to provide custom functionalities. For example, you can create a custom save() or __init__ method, so Django executes custom logic when saving or initializing a Django model instance, respectively.

While this ability provides a wide array of possibilities to execute custom logic at certain points in the life-cycle of a Django model instance (e.g. create an audit trail if the delete() method is called on an instance), there are cases that require executing custom logic when an event happens in the life-cycle of another model instance. For example, updating an Item model's stock value when an Order model instance is saved or generating a Customer model instance each time a Contact model instance is updated.

These scenarios create an interesting implementation problem. One approach is to interconnect the logic between two model classes to fulfill this type of logic (e.g. every time an Order object is saved, update related Item objects). This last approach though can become overly complex with more demanding requirements, because dependent classes need to be updated to perform actions on behalf of other classes. This type of problem only grows in complexity once you confront the need to execute actions on classes you have no control over (e.g. how do you trigger a custom action when an instance of the built-in django.contrib.auth.User model class is saved ?).

It turns out this scenario to trigger actions on behalf of other classes is so common in software, there's a name for it: the observer pattern[8]. The Django framework supports the observer pattern through Django signals.

In the simplest terms, signals are emitted by Django models into the project environment, just like airplanes emit signals into the environment. Similarly, all you need to intercept signals is create the appropriate receiver to intercept such signals (e.g. a receiver to detect the save signal for all project models, a receiver to detect the delete signal for a specific model) and execute whatever custom logic it's you need to run whenever the signal surfaces.

Built-in Django model signals

By default, all Django models emit signals for their most important workflow events. This is a very important fact for the sole reason it provides a noninvasive way to link into the events of any Django model. Note the emphasis on any Django model, meaning your project models, third-party app models and even Django built-in models, this is possible because signals are baked-in into the core django.db.models.Model class used by all Django models. Table 7-5 illustrates the various signals built-in to Django models.

Table 7-5. Built-in Django model signals

Signal(s) Signal class Description
pre_initpost_init django.db.models.signals.pre_initdjango.db.models.signals.post_init Signal emitted at the beginning and end of the model's __init__() method.
pre_savepost_save django.db.models.signals.pre_savedjango.db.models.signals.post_save Signal emitted at the beginning and end of the model's __save__() method.
pre_deletepost_delete django.db.models.signals.pre_deletedjango.db.models.signals.post_delete Signal emitted at the beginning and end of the model's __delete__() method.
m2m_changed django.db.models.signals.m2m_changed Signal emitted when a ManyToManyField is changed on a model instance.
class_prepared django.db.models.signals.class_prepared Signal emmited when a model has been defined and registered with Django's model system. Used internally by Django, but rarely used for other circumstances.
Tip Django also offers built-in signals for requests, responses, pre & post migrate events and testing events. See built-in signal reference[9].

Now that you know there are always a series of signals emitted by all of your Django project models. Let's see how to get notified of signals, in order to execute custom logic when the signal occurs.

Listen for Django model signals

Listening for Django model signals -- and Django signals in general -- follows a straightforward syntax using the @receiver decorator from the django.dispatch package, as shown in listing 7-37

Listing 7-37. Basic syntax to listen for Django signals

from django.dispatch import receiver

@receiver(<signal_to_listen_for_from_django_core_signals>,sender=<model_class_to_listen_to>)
def method_with_logic_to_run_when_signal_is_emitted(sender, **kwargs):
      # Logic when signal is emitted
      # Access sender & kwargs to get info on model that emitted signal

As you can in listing 7-37, you enclose the logic you want to execute on a signal in a Python method that follows the input signature of the signal callback, this in turn allows you to access information about the model that emitted the signal. For most signals, the input signature sender, **kwargs fits, but this can change depending on the signal -- see the footnote reference on signals for details on the input arguments used by each signal.

Once you have a method to run on a signal emission, the method must be decorated with the @receiver annotation, which generally uses two arguments: a signal to listen for -- those described in table 7-5 -- which is a required argument and the optional sender argument to specify which model class to listen into for the signal. If you want to listen for the same signal emitted by all your project's models -- a rare case -- you can omit the sender argument.

Now that you have a basic understanding of the syntax used to listen for Django signals, let's explore the placement and configuration of signals in a a Django project.

The recommended practice is to place signals in a file called signals.py under an app's main folder (i.e. alongside models.py, views.py). This keeps signals in an obvious location, but more importantly it avoids any potential interference (e.g. loading issues, circular references) given signals can contain logic related to models and views. Listing 7-38 illustrates the contents of the signals.py file for an app named items.

Listing 7-38. Listen for Django pre_save signal on Item model in signals.py

from django.dispatch import receiver
from django.db.models.signals import pre_save
from django.dispatch import receiver

import logging
stdlogger = logging.getLogger(__name__)

@receiver(pre_save, sender='items.Item')
def run_before_saving(sender, **kwargs):
    stdlogger.info("Start pre_save Item in signals.py under items app")
    stdlogger.info("sender %s" % (sender))
    stdlogger.info("kwargs %s" % str(kwargs))

First, notice the signal listening method in listing 7-38 uses the @receiver decorator to listen for the pre_save signal on the Item model. This means that every time an Item model instance is about to be saved, the method run_before_saving is triggered. In this case, a few log messages are generated, but the method can execute any logic depending on requirements.

Tip The sender argument in listing 7-38 uses a string model reference instead of a standard class import reference. This ensures models in signals are lazy-loaded avoiding potential import conflicts between models and signals.

Once you have a signals.py file with all its signal listening methods, you must tell Django to inspect this file to load the signal logic. The recommended practice is to do this with an import statement in the apps.py file which is also part of an app's structure. Listing 7-39 illustrates a modified version of the default apps.py to inspect the signals.py file.

Listing 7-39. Django apps.py with custom ready() method to load signals.py

from django.apps import AppConfig
class ItemsConfig(AppConfig):
    name = 'coffeehouse.items'
    
    def ready(self):
        import coffeehouse.items.signals

In listing 7-39 you can see the apps.py file contains the ready() method. The ready() method as its name implies, is executed once the app is ready to be accessed. Inside ready() there's an import statement for the signals module in the same app (i.e. listing 7-38), which in turn makes Django load the signal listening methods in listing 7-38.

In addition to this change to the apps.py file to load signals, it's also necessary to ensure the apps.py file itself is loaded by Django. For this requirement there are two options illustrated in listing 7-40.

Listing 7-40. Django configuration options to load apps.py

# Option 1) Declare apps.py class as part of INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
    'coffeehouse.items.apps.ItemsConfig',
     ...    
]

# Option 2) Declare default_app_config inside the __init__ file of the app
# /coffeehouse/items/__init__.py
default_app_config = 'coffeehouse.items.apps.ItemsConfig'

The first option in listing 7-40 consists of explicitly declaring an app's configuration class as part of INSTALLED_APPS -- in this case coffeehouse.items.apps.ItemsConfig -- instead of the standalone package app statement (e.g. coffeehouse.items). This last variation ensures the custom ready() method is called as part of the initialization procedure.

The second option in listing 7-40 consists of adding the default_app_config value to the __init__ file of an app (i.e. the one besides the apps.py, models.py and views.py) and declaring the app's configuration class, in this case coffeehouse.items.apps.ItemsConfig.

The first option in listing 7-40 is newer and supported since the introduction of app configuration in Django 1.9, the second is equally valid and was used prior to introduction of app configuration.

Emit custom signals in Django model signals

In addition to the Django built-in signals presented in table 7-5, it's also possible to create custom signals. Custom signals are helpful when you want to execute actions pegged to important events in the workflow of your own models (e.g. when a store closes, when an order is created), where as Django built-in signals let you listen to important Django model workflow signals (e.g. before and after a model instance is saved or deleted).

The first step to create custom signals is to generate a signal instance with the django.dispatch.Signal class. The Signal class only requires the providing_args argument, which is a list of arguments that both signal emitters and receivers expect the Signal to have. The following snippet illustrate two custom Signal instances:

from django.dispatch import Signal

order_complete = Signal(providng_args=["customer","barista"])
store_closed = Signal(providing_args=["employee"]) 

Once you have a custom Signal instance, the next step is to add a signal emission method to trigger a Signal instance. For Django models, the standard location is to emit signals as part of a class method, as illustrated in listing 7-41.

Listing 7-41. Django model emitting custom signal

from django.db import models
from coffeehouse.stores.signals import store_closed

class Store(models.Model):
    name = models.CharField(max_length=30)    
    address = models.CharField(max_length=30,unique=True)
    ...
    def closing(self,employee):   
        store_closed.send(sender=self.__class__, employee=employee)
     

As you can see in listing 7-41, the Store model defines a method called closing() which accepts an employee input. Inside this closing() method, a signal is emitted to the custom Signal class named store_closed using the send() method -- inherited through Signal -- which uses the arguments expected by the custom Signal class.

Next, when you have a reference to a Store model instance and call the closing() method on any store instance (e.g. downtown_store.closing(employee=request.user)) a custom store_closed signal is emitted. And who receives this signal ? Anyone who is listening for it, just like built-in Django signals. The following snippet illustrates a signal listening method for the custom store_closed signal:

@receiver(store_closed)
def run_when_store_is_closed(sender,**kwargs):
    stdlogger.info("""Start store_closed Store 
                  in signals.py under stores app""")
    stdlogger.info("sender %s" % (sender))
    stdlogger.info("kwargs %s" % str(kwargs))

This last listening signal method is almost identical to the ones used to listen for built-in Django signals -- presented in the past section in listing 7-38. In this case, the only argument to the @receiver decorator corresponds to the signal name store_closed, which indicates the method is listening for this signal. Since the custom store_closed signal is produced in a limited location (i.e. in a single model method) -- unlike built-in signals which are produced by all models -- the @receiver decorator forgoes adding the optional sender argument.

  1. https://en.wikipedia.org/wiki/Observer_pattern     

  2. https://docs.djangoproject.com/en/1.11/ref/signals/