How to Set Up Notifications in Django Using Signals: A Step-by-Step Guide

In Django, signals are used to send and receive important information when saving, changing or even deleting a data model, and this refers to certain past or present events in real time. Signals help us associate events with actions. My name is Yasin, I am a junior Python developer at Kokoc Group, I have been working there for a little over a year. I study and use Django and FastAPI frameworks in my work. Today I will show an example of how you can effectively use signals, but I expect that you have a basic understanding of Python 3, virtual environment and setting up a Django project version 3 or higher. Let's go!

Introduction to Django signals

What are they needed for?

A signal in Django is a method of handling tasks before or after registered events are ready to be updated. Depending on the timing of the signal, events can be handled either before or after they are completed. Signals in Django allow handlers to be notified of specific events that occur in different components of the application. This helps developers set up automation for different events. For example, the process of sending an email after a user has successfully registered can be separated from the work of the main application.

Signals can be used for various purposes, such as:

  • Sending notifications to users and managers when data changes.

  • Logging changes to the database.

  • Updating related models.

  • Integration with external systems.

Let's turn to the official Django documentation, namely to the signals sectionTo receive a signal, you must register a receiver function using the Signal.connect() dispatcher method. The receiver function is called when the signal is sent. All signal receiver functions are called one at a time, in the order they are registered.

The Signal.connect method has the following syntax:

  • receiver: The receiver function that will be connected to this signal.

  • sender: Specifies a specific sender from which signals will be received.

  • weak: Django stores signal handlers as weak references by default. So if your receiver is a local function, it may be garbage collected. To prevent this, set weak=False when calling the connect() method.

  • dispatch_uid: Unique identifier for the receiving function in cases where duplicate signals may be sent.

Signal Dispatchers

Dispatchers are built-in connect() and disconnect() methods of Django signals that connect or disconnect receiver functions with various parameters. They notify when a certain action is completed.

To register a receiver function that is called by signals, use the Django signal dispatcher's connect() method. For example, let's create a receiver function and connect it to a signal that is triggered when an HTTP request is sent.

The receiver function get_notified() displays a notification on the screen. It is a receiver (receiver of a signal) because it expects sender in its arguments. model classwhich should have the signal called.

Now let's connect the receiver to the dispatcher. There are two ways to do this. The first way is to import the request_started class from Django signals and pass the get_notified receiver function when calling the connect() method on request_started, as shown in the screenshot below.

Registering Receiver Functions with Decorators

Another way to register a receiver function is to use decorators. In short, decorators are wrapper functions that return another inner function that is abstracted from use outside the decorator context. This means that the receiver function will be passed to an inner method, where it will be registered with the Django signals system via the familiar connect() method. Here is an implementation using the @reciever decorator:

How the @receiver decorator works:

  • The @receiver decorator actually calls the connect() method internally.

  • The method registers the receiver function in the Django signals system.

  • When the request_started signal is fired, Django calls all registered receiver functions, including get_notified.

Implementing Signals in Django

Let's imagine that we are developing the GreenBerries marketplace and have received a task to implement notifications for partners about reviews of their product. But how to track the creation of a review or any other object?

There are 3 types of Django model signals:

  • pre_init/post_init : when initializing a class instance (__init__() method)

  • pre_save/post_save : when changing the object data, and using the save() method

  • pre_delete/post_delete : when deleting a model instance (delete() method)

We will use the post_save signal to implement a mechanism whereby after a review is saved, a notification is created for the seller. The seller will be able to track the popularity of the product card and maintain feedback with their customers.

For this we will prepare 4 models:

  • User — built-in model “User”, to which the roles “Client” (buyer) and “Partner” (seller) have been added;

  • Product — a “Product” model that has a name, description, text and seller;

  • Review — a “Review” model that has a connection with the Product and the buyer, as well as the text, rating and comment of the seller;

  • Notification – a “Notification” model that has a recipient, text, and importance level.

# marketplace/models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import models

# User: пользовательская модель с ролями "клиент" и "партнер".
class User(AbstractUser):
   CLIENT = 'client'
   PARTNER = 'partner'
   ROLE_CHOICES = [
       (CLIENT, 'Client'),
       (PARTNER, 'Partner'),
   ]

   role = models.CharField(max_length=7, choices=ROLE_CHOICES)

# Product: модель продукта, связанная с продавцом.
class Product(models.Model):
   name = models.CharField(max_length=255)
   description = models.TextField()
   owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="products")

   def __str__(self):
       return self.name

# Review: модель отзыва, связанная с продуктом, содержащая текст, рейтинг и ответ от партнера.
class Review(models.Model):
   product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="reviews")
   user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="reviews")
   text = models.TextField()
   rating = models.PositiveSmallIntegerField(choices=[(i, str(i)) for i in range(1, 6)])
   partner_response = models.TextField(blank=True, null=True)

   def __str__(self):
       return f'Review of {self.product.name} by {self.user.username}'

# Notification: модель уведомлений с получателем и текстом.
class Notification(models.Model):
   INFORMATIVE = 'informative'
   ATTENTION = 'attention'
   CRITICAL = 'critical'
   LEVEL_CHOICES = [
       (INFORMATIVE, 'Informative'),
       (ATTENTION, 'Attention'),
       (CRITICAL, 'Critical'),
   ]

   recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
   text = models.TextField()
   level = models.CharField(max_length=12, choices=LEVEL_CHOICES, default=INFORMATIVE)

   def __str__(self):
       return f'Notification for {self.recipient.username} ({self.get_level_display()})'

We have everything ready to create a signal, and then a task comes from the business, which sounds like this: “It is necessary to inform partners about new reviews of their products.” Excellent! We are familiar with signals, which means we have a solution in our pocket, namely – when saving the “Review” object, we need to create an informational “Notification” for the seller.

# marketplace/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Review, Notification, User
 
@receiver(post_save, sender=Review)
def create_notification_for_partner(sender, instance, created, **kwargs):
   if created:
       product = instance.product
       owner = product.owner
       review_notification_text = f'New review for your product "{product.name}" by {instance.user.username}.'
       Notification.objects.create(
           recipient=owner,
           text=review_notification_text,
           level=Notification.INFORMATIVE
       )

Let's go through the entire method line by line (fortunately, it's short):

@receiver(post_save, sender=Review)

The @receiver decorator connects the create_notification_for_partner function to the post_save signal of the Review model. This means that the function will be called every time an instance of the Review model is saved.

def create_notification_for_partner(sender, instance, created, **kwargs):

This is the definition of the create_notification_for_partner function, which takes four parameters:

  • sender: the model that sent the signal (in this case, Review).

  • instance: The instance of the Review model that was saved.

  • created: A boolean value indicating whether a new instance was created (True) or whether it was an update to an existing one (False).

  • **kwargs: additional arguments that may be passed to the signal.

if created:
    product = instance.product
    owner = product.owner

We check if a new Review instance has been created. If created is True, then this is a new review, and the code inside this block will be executed. We retrieve the product associated with this review (instance.product). And we retrieve the owner of the product (product.owner).

review_notification_text = f'New review for your product "{product.name}" by {instance.user.username}.’
Notification.objects.create(recipient=owner, text=review_notification_text, level=Notification.INFORMATIVE)

We form the text of the notification about the new review. And we create a new notification with the INFORMATIVE level for the product owner, informing about the new review. We set the text of the notification and associate it with the recipient (owner).

Registering signals when starting an application

Make sure the signal is imported and registered when the application starts. For example, you can create a signals.py file in your application and import it in the ready method of the application configuration class. An example is below:

# marketplace/apps.py
from django.apps import AppConfig
 
class MarketplaceConfig(AppConfig):
   name="marketplace"
 
  def ready(self):
       import marketplace.signals

This will ensure that the signal is registered when the Django application starts.

Complicating notification logic

Now let's complicate the logic a little and in addition to the notification about a new review in the example above, we will warn the seller about the decrease in popularity of the product card. If the product has a rating and the arithmetic mean of the rating is less than or equal to 4.5, then we will create a notification for the seller with the ATTENTION level. Here is what the code looks like after adding the condition.

# marketplace/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db.models import Avg
from .models import Review, Notification, User

@receiver(post_save, sender=Review)
def create_notification_for_partner(sender, instance, created, **kwargs):
   if created:
       product = instance.product
       owner = product.owner

       # Рассчитываем средний рейтинг продукта и получаем по ключу rating__avg число
       average_rating = product.reviews.aggregate(Avg('rating')).get('rating__avg', None)

       # Проверяем, если средний рейтинг меньше 4.5
       if average_rating is not None and average_rating <= 4.5:
           notification_text = f'Attention: The average rating of your product "{product.name}" has fallen below 4.5.'
           Notification.objects.create(
               recipient=owner,
               text=notification_text,
               level=Notification.ATTENTION
           )

       # Создаем уведомление для нового отзыва
       review_notification_text = f'New review for your product "{product.name}" by {instance.user.username}.'
       Notification.objects.create(
           recipient=owner,
           text=review_notification_text,
           level=Notification.INFORMATIVE
       )

As you can see, nothing complicated.

Conclusion

Signals in Django allow developers to create powerful and flexible notification and automation systems by reacting to various events in the application. They help to separate tasks into separate modules, which makes the code more organized and easier to maintain. For example, sending notifications to sellers about new reviews or changes in product popularity can be implemented easily and efficiently.

Thus, we can do various checks in the signal and not only create notification objects, but implement other logic. This opens up wide opportunities for automation and improving user experience. You can give other examples in the comments how you use/used signals in your practice.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *