В Django сигналы используются для отправки и получения важной информации при сохранении, изменении или даже удалении модели данных и это относится к определенным прошлым или настоящим событиям в реальном времени. Сигналы помогают нам связывать события с действиями. Меня зовут Ясин, я младший разработчик Python в Kokoc Group, работаю чуть больше года. Изучаю и использую в работе фреймворки Django и FastAPI. Сегодня покажу пример, как можно эффективно использовать сигналы, но ожидаю, что вы имеете базовые представления о Python 3, виртуальной среде и настройке проекта Django  версии 3 или выше. Поехали!

Введение в сигналы Django

Зачем они нужны?

Сигнал в Django — это метод обработки задач до или после того, как зарегистрированные события готовы к обновлению. В зависимости от времени выполнения сигнала, события могут быть обработаны либо до, либо после их завершения. Сигналы в Django позволяют обработчикам узнавать об определенных событиях, которые происходят в различных компонентах приложения. Это помогает разработчикам настраивать автоматизацию для различных событий. Например, процесс отправки электронного письма после успешной регистрации пользователя может быть отделен от работы основного приложения.

Сигналы могут использоваться для различных целей, таких как:

  • Отправка уведомлений пользователям, менеджерам при изменении данных.

  • Логирование изменений в базе данных.

  • Обновление связанных моделей.

  • Интеграция с внешними системами.

Обратимся к официальной документации Django, а именно к разделу сигналов, чтобы принимать сигнал, необходимо зарегистрировать функцию-получатель (ресивер) с помощью метода-диспетчера Signal.connect(). Функция-получатель вызывается, когда сигнал отправляется. Все функции-получатели сигнала вызываются по одной за раз, в порядке их регистрации.

Метод Signal.connect имеет следующий синтаксис:

  • receiver: Функция-ресивер, которая будет подключена к этому сигналу.

  • sender: Указывает конкретного отправителя, от которого будут приниматься сигналы.

  • weak: Django по умолчанию сохраняет обработчики сигналов как слабые ссылки. Таким образом, если ваш ресивер является локальной функцией, он может быть собран сборщиком мусора. Чтобы предотвратить это, установите weak=False при вызове метода connect().

  • dispatch_uid: Уникальный идентификатор для функции-получателя в случаях, когда могут быть отправлены дублирующиеся сигналы.

Диспетчеры сигналов

Диспетчеры — это встроенные методы connect() и disconnect() сигналов Django, которые выполняют подключение или отключение функций-ресиверов с различными параметрами. Они уведомляют, когда определенное действие завершено.

Чтобы зарегистрировать функцию-ресивер, которая вызывается сигналами, используйте метод диспетчера сигналов Django connect(). Например, давайте создадим функцию-ресивер и подключим ее к сигналу, который срабатывает при отправке HTTP-запроса.

Функция-ресивер get_notified() выводит уведомление на экран. Она является ресивером (получателем сигнала), так как ожидает в аргументах sender модель-класс, у которого должен быть вызван сигнал.

Теперь давайте подключим ресивер к диспетчеру. Существует два способа сделать это. Первый способ — импортировать класс request_started из сигналов Django и передать функцию-ресивер get_notified при вызове метода connect() у request_started, как показано на скриншоте ниже.

Регистрация функций-ресиверов с помощью декораторов

Другой способ зарегистрировать функцию-ресивер — это использовать декораторы. Если коротко, декораторы — это функции-обертки, возвращающие другую внутреннюю функцию, которая абстрагирована от использования вне контекста декоратора. Это означает, что функция-ресивер будет передана во внутренний метод, где через известный нам метод connect() будет зарегистрирована в системе сигналов Django. Вот вариант реализации через декоратор @reciever:

Как работает декоратор @receiver:

  • Декоратор @receiver фактически вызывает метод connect() внутри себя.

  • Метод регистрирует функцию-ресивер в системе сигналов Django.

  • Когда срабатывает сигнал request_started, Django вызывает все зарегистрированные функции-ресиверы, включая get_notified.

Реализация сигналов в Django

Представим, что мы разрабатываем маркетплейс GreenBerries и поступила задача реализовать нотификацию для партнеров об отзывах на их товар. Но как отслеживать создание отзыва или какого-либо другого объекта?

Существует 3 типа сигналов моделей Django:

  • pre_init/post_init : при инициализации экземпляра класса (метод __init__())

  • pre_save/post_save : при изменении данных объекта, и использование метода save()

  • pre_delete/post_delete : при удалении экземпляра модели (метод delete())

Мы воспользуемся сигналом post_save, реализовав механизм, при котором после сохранения отзыва, создается уведомление для продавца. Он сможет отслеживать популярность товарной карточки и поддерживать обратную связь со своими клиентами.

Для этого подготовим 4 модели: 

  • User — встроенная модель «Пользователь», куда были добавлены роли «Клиент» (покупатель) и «Партнер» (продавец);

  • Product — модель «Продукт», у которой есть название, описание, текст и продавец;

  • Review — модель «Отзыв», у которой есть связь с Продуктом и покупателем, а также текст, рейтинг и комментарий продавца;

  • Notification — модель «Уведомление», у которой есть получатель, текст и уровень важности.

# 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()})'

У нас готово все для создания сигнала, и вот поступает задача от бизнеса, которая звучит так: «Необходимо сообщать партнерам о новых отзывах на их товары». Отлично! Мы знакомы с сигналами, а значит решение у нас в кармане, а именно — при сохранении объекта «Отзыв» нам нужно создать информационное «Уведомление» для продавца.

# 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
       )

Давайте разберем построчно весь метод (благо, он короткий):

@receiver(post_save, sender=Review)

Декоратор @receiver подключает функцию create_notification_for_partner к сигналу post_save модели Review. Это означает, что функция будет вызываться каждый раз после того, как экземпляр модели Review будет сохранен.

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

Это определение функции create_notification_for_partner, которая принимает четыре параметра: 

  • sender: модель, отправившая сигнал (в данном случае, Review). 

  • instance: экземпляр модели Review, который был сохранен. 

  • сreated: булевое значение, указывающее, был ли создан новый экземпляр (True), или это было обновление существующего (False). 

  •  **kwargs: дополнительные аргументы, которые могут быть переданы сигналом.

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

Проверяем, был ли создан новый экземпляр Review. Если created в состоянии True, значит это новый отзыв, и код внутри этого блока будет выполнен. Извлекаем продукт, связанный с данным отзывом (instance.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)

Формируем текст уведомления о новом отзыве. И создаем новое уведомление с уровнем INFORMATIVE для владельца продукта, информируя о новом отзыве. Устанавливаем текст уведомления и связываем его с получателем (owner).

Регистрация сигналов при старте приложения

Убедитесь, что сигнал импортируется и регистрируется при запуске приложения. Например, вы можете создать файл signals.py в вашем приложении и импортировать его в методе ready класса конфигурации приложения. Пример ниже:

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

Это обеспечит регистрацию сигнала при старте приложения Django.

Усложнение логики уведомлений

Теперь давайте немного усложним логику и кроме уведомления о новом отзыве в примере выше будем предупреждать продавца о снижении популярности карточки товара. Если у товара есть рейтинг и среднее арифметическое число рейтинга меньше, либо равно 4.5, то создадим уведомление для продавца с уровнем ATTENTION. Вот как выглядит код, после добавления условия.

# 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
       )

Как видите, ничего сложного.

Заключение

Сигналы в Django позволяют разработчикам создавать мощные и гибкие системы уведомлений и автоматизации, реагируя на различные события в приложении. Они помогают выделить задачи в отдельные модули, что делает код более организованным и легким для сопровождения. Например, отправка уведомлений продавцам о новых отзывах или изменениях в популярности товаров может быть реализована легко и эффективно.

Таким образом, мы можем делать различные проверки в сигнале и не только создавать объекты уведомлений, но имплементировать другую логику. Это открывает широкие возможности для автоматизации и улучшения пользовательского опыта. Можете в комментариях привести другие примеры, как вы используете/использовали сигналы в своей практике.

Комментарии (0)