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 passed
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.

Create Employee
creating an Employee record
EmployeeProfile created
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.