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.