Exploring Django Signals
Events are ubiquitous in our everyday lives, and so are they in the software lifecycle. An event, in the context of a software lifecycle, can be any action or activity that is recognized, identified or tracked by the system and usually alters the state of a system or a portion of the system. In some cases, we would like to emit some other event or perform an action just before or after a specific event occurs. Django provides signals and receivers (pre_save and post_save) for such cases; the receiver listens for a specific event or signal from a known sender and performs an action based on that. In this post, we’ll explore this feature for a simple use case: creating a related EmployeeProfile
immediately after an Employee
object is created.
We first create the models for Employee
and EmployeeProfile
with a OneToOne relation as follows:
// models.py
from django.db import models, transaction
from django.db.models import OneToOneField
from django.dispatch.dispatcher import receiver
from django_extensions.db.models import TimeStampedModel
from django.db.models.signals import post_save
from phonenumber_field.modelfields import PhoneNumberField
class Employee(TimeStampedModel):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
phone = PhoneNumberField(unique=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class EmployeeProfile(TimeStampedModel):
class Teams(models.TextChoices):
Product = "product", "Product"
Engineering = "engineering", "Engineering"
Marketing = "marketing", "Marketing"
class Genders(models.TextChoices):
Female = "female", "Female"
Male = "male", "Male"
Other = "other", "Other"
dob = models.DateField(blank=True, null=True)
team = models.CharField(
max_length=50, choices=Teams.choices,
blank=True, null=True
)
address = models.CharField(blank=True, null=True)
gender = models.CharField(
max_length=30, choices=Genders.choices,
blank=True, null=True
)
# related_name will be used in reverse access in the test
employee = OneToOneField(
Employee, related_name="profile",
on_delete=models.deletion.PROTECT
)
def __str__(self):
return f"{self.employee}"
Let’s proceed to add a post_save receiver
which will listen for create events on the Employee model.
// models.py
@receiver(
post_save,
dispatch_uid="create_employee_profile",
sender=Employee
)
def create_employee_profile(instance, created, **kwargs):
if created:
with transaction.atomic():
EmployeeProfile.objects.create(employee=instance)
In the code snippet above, we add a Python function, create_employee_profile
, but this function has a @receiver
decorator with post_save (signal type), dispatch_uid (a unique receiver id) and a sender (model to listen on) as args. This makes it a post_save signal listening for ‘create’ events on the Employee
model. The function itself takes ‘instance’ and ‘created’ as arguments as well as optional keyword arguments. In the function, there is a check for the creation of an Employee object after which a related EmployeeProfile
object is immediately created using the instance (Employee) as the related Employee object. This function is automatically invoked whenever a new Employee object is created. If we wanted to listen to another event such as an ‘update’ event, the check would have been this instead:
if not created:
...
Let’s add a simple test to validate our assertion.
from rest_framework.test import APITestCase
from django_dynamic_fixture import G
from .models import Employee
class TestEmployee(APITestCase):
def test_employee_creation_auto_creates_related_profile(self):
"""
test EmployeeProfile object created
on Employee object post save
:return:
"""
# persist an Employee object
emp_obj = G(
Employee, first_name="John",
last_name="Doe", email="[email protected]"
)
# reload Employee model values from the database
emp_obj.profile.refresh_from_db()
# access EmployeeProfile via related_name (reverse access)
# and assert related Employee object attributes
self.assertEqual(emp_obj.profile.employee.first_name, "John")
self.assertEqual(emp_obj.profile.employee.last_name, "Doe")
self.assertEqual(emp_obj.profile.employee.email, "[email protected]")
In this simple test, an Employee is created and persisted in the database using the django dynamic fixture’s G object. We then reload new changes from the database, proceed to access the auto created EmployeeProfile via the reverse (related_name) OneToOne relationship - profile and then assert that the related profile object has the expected attributes.
test result |
For a manual test using the Django admin portal, when an Employee object is created, a related Employee object is also automatically created as displayed below.
creating an Employee record |
auto-created EmployeeProfile |
This is just one out of the numerous use cases for Django Signals. You can learn more about them and their different available use cases from the official documentation. The project used in this post can also be found here.