![](https://habrastorage.org/getpro/habr/upload_files/f17/e3f/d0c/f17e3fd0c6aec545a21065814ccce726.jpg)
В Django сигналы используются для отправки и получения важной информации при сохранении, изменении или даже удалении модели данных и это относится к определенным прошлым или настоящим событиям в реальном времени. Сигналы помогают нам связывать события с действиями. Меня зовут Ясин, я младший разработчик Python в Kokoc Group, работаю чуть больше года. Изучаю и использую в работе фреймворки Django и FastAPI. Сегодня покажу пример, как можно эффективно использовать сигналы, но ожидаю, что вы имеете базовые представления о Python 3, виртуальной среде и настройке проекта Django версии 3 или выше. Поехали!
Введение в сигналы Django
Зачем они нужны?
Сигнал в Django — это метод обработки задач до или после того, как зарегистрированные события готовы к обновлению. В зависимости от времени выполнения сигнала, события могут быть обработаны либо до, либо после их завершения. Сигналы в Django позволяют обработчикам узнавать об определенных событиях, которые происходят в различных компонентах приложения. Это помогает разработчикам настраивать автоматизацию для различных событий. Например, процесс отправки электронного письма после успешной регистрации пользователя может быть отделен от работы основного приложения.
![](https://habrastorage.org/getpro/habr/upload_files/6b9/137/71c/6b913771cc0820b99ea3df488b0ecc25.jpg)
Сигналы могут использоваться для различных целей, таких как:
Отправка уведомлений пользователям, менеджерам при изменении данных.
Логирование изменений в базе данных.
Обновление связанных моделей.
Интеграция с внешними системами.
Обратимся к официальной документации Django, а именно к разделу сигналов, чтобы принимать сигнал, необходимо зарегистрировать функцию-получатель (ресивер) с помощью метода-диспетчера Signal.connect(). Функция-получатель вызывается, когда сигнал отправляется. Все функции-получатели сигнала вызываются по одной за раз, в порядке их регистрации.
![](https://habrastorage.org/getpro/habr/upload_files/c73/353/59e/c7335359ef47c518908ffc73e2840a09.png)
Метод Signal.connect имеет следующий синтаксис:
receiver: Функция-ресивер, которая будет подключена к этому сигналу.
sender: Указывает конкретного отправителя, от которого будут приниматься сигналы.
weak: Django по умолчанию сохраняет обработчики сигналов как слабые ссылки. Таким образом, если ваш ресивер является локальной функцией, он может быть собран сборщиком мусора. Чтобы предотвратить это, установите weak=False при вызове метода connect().
dispatch_uid: Уникальный идентификатор для функции-получателя в случаях, когда могут быть отправлены дублирующиеся сигналы.
Диспетчеры сигналов
Диспетчеры — это встроенные методы connect() и disconnect() сигналов Django, которые выполняют подключение или отключение функций-ресиверов с различными параметрами. Они уведомляют, когда определенное действие завершено.
Чтобы зарегистрировать функцию-ресивер, которая вызывается сигналами, используйте метод диспетчера сигналов Django connect(). Например, давайте создадим функцию-ресивер и подключим ее к сигналу, который срабатывает при отправке HTTP-запроса.
![](https://habrastorage.org/getpro/habr/upload_files/cda/680/76f/cda68076fb5ac9f446811610848a6455.png)
Функция-ресивер get_notified() выводит уведомление на экран. Она является ресивером (получателем сигнала), так как ожидает в аргументах sender модель-класс, у которого должен быть вызван сигнал.
Теперь давайте подключим ресивер к диспетчеру. Существует два способа сделать это. Первый способ — импортировать класс request_started из сигналов Django и передать функцию-ресивер get_notified при вызове метода connect() у request_started, как показано на скриншоте ниже.
![](https://habrastorage.org/getpro/habr/upload_files/ff9/e7a/4c0/ff9e7a4c0d1831e47e7b56ec7424a7f3.png)
Регистрация функций-ресиверов с помощью декораторов
Другой способ зарегистрировать функцию-ресивер — это использовать декораторы. Если коротко, декораторы — это функции-обертки, возвращающие другую внутреннюю функцию, которая абстрагирована от использования вне контекста декоратора. Это означает, что функция-ресивер будет передана во внутренний метод, где через известный нам метод connect() будет зарегистрирована в системе сигналов Django. Вот вариант реализации через декоратор @reciever:
![](https://habrastorage.org/getpro/habr/upload_files/c52/dd1/23d/c52dd123d63d8742c71cde8f54652d51.png)
Как работает декоратор @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 позволяют разработчикам создавать мощные и гибкие системы уведомлений и автоматизации, реагируя на различные события в приложении. Они помогают выделить задачи в отдельные модули, что делает код более организованным и легким для сопровождения. Например, отправка уведомлений продавцам о новых отзывах или изменениях в популярности товаров может быть реализована легко и эффективно.
Таким образом, мы можем делать различные проверки в сигнале и не только создавать объекты уведомлений, но имплементировать другую логику. Это открывает широкие возможности для автоматизации и улучшения пользовательского опыта. Можете в комментариях привести другие примеры, как вы используете/использовали сигналы в своей практике.