Модуль 20

Signals у Django

Ось твій урок про Django Signals, створений у стилі CS50: енергійний, з аналогіями та акцентом на "чому це важливо".


🎓 CS50: Django Signals — Магія подій "за кадром"

Всім привіт! Це CS50, і сьогодні ми поговоримо про те, як змусити ваш код спілкуватися між собою, не перетворюючи його на спагеті.

1. 🔥 Вступ: Проблема "Все в одному місці"

Уявіть, що ви будуєте величезний інтернет-магазин. У вас є модель Order (Замовлення). Коли клієнт натискає кнопку "Купити", відбувається збереження замовлення в базу даних. Все просто, правда?

Але зачекайте! Що ще має статися в цей момент? 1. Потрібно надіслати email клієнту ("Дякуємо за покупку!"). 2. Потрібно списати товар зі складу. 3. Потрібно нарахувати бонуси лояльності. 4. Потрібно надіслати повідомлення менеджеру в Slack.

Де ми напишемо цей код? У методі save() моделі? У views.py?

Риторичне запитання: Якщо ми запихнемо всю цю логіку в метод save() замовлення, то наша модель Order раптом повинна знати про email-сервер, про Slack, про склад і про систему бонусів. Чи це хороша архітектура?

Звісно, ні! Це називається High Coupling (сильна зв’язність). Якщо зламається Slack, ваше замовлення може не зберегтися. Це катастрофа.

Аналогія: Уявіть, що ви купуєте каву. Бариста робить каву. Чи повинен бариста сам бігти на вулицю і кричати перехожим "Ми продали ще одну чашку!", потім бігти в бухгалтерію записувати прибуток, а потім дзвонити постачальнику молока? Ні. Бариста просто каже: "Замовлення готове!". А інші люди (або системи), почувши це, роблять свою роботу.

Ось тут на сцену виходять Django Signals. Це спосіб сказати: "Гей, щось трапилося!", і дозволити іншим частинам коду відреагувати на це, не знаючи одне про одного.


2. 🧠 Теоретична база: Як це працює "під капотом"

Сигнали — це реалізація патерну Observer (Спостерігач) або Pub/Sub (Publisher/Subscriber).

У нас є три головні дійові особи:

  1. Sender (Відправник): Той, хто кричить "Подія сталася!" (наприклад, модель User, коли її зберегли).
  2. Signal (Сигнал): Саме "радіоповідомлення", яке летить в ефір.
  3. Receiver (Отримувач): Функція, яка "підписана" на цей сигнал. Вона чекає, поки він прилетить, і тоді виконує код.

⚠️ Що треба обов'язково запам'ятати:

Сигнали в Django — СИНХРОННІ!

Це найпоширеніша помилка новачків. Коли ви відправляєте сигнал, Django зупиняє виконання основного потоку, біжить виконувати всі функції-отримувачі, і тільки коли вони завершаться, продовжує роботу.

Інтуїтивно: Це не як відправити листа поштою (кинув і забув). Це як передати естафетну паличку. Поки функція-отримувач не закінчить роботу, ваше збереження в базу даних (або відповідь користувачу) буде "висіти".

Найпопулярніші вбудовані сигнали:

  • pre_save / post_save — перед або після збереження об'єкта.
  • pre_delete / post_delete — перед або після видалення.
  • m2m_changed — коли змінюється Many-to-Many поле.

3. 🧪 Приклади: Від простого до реального

Приклад 1: "Hello World" у світі сигналів

Давайте створимо функцію, яка просто друкує в консоль повідомлення кожного разу, коли створюється або оновлюється будь-який користувач.

Припустимо, у нас є файл signals.py:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

# @receiver — це декоратор, який "підписує" функцію на сигнал
@receiver(post_save, sender=User)
def announce_user_save(sender, instance, created, **kwargs):
    if created:
        print(f"🎉 Ура! Новий юзер {instance.username} щойно народився!")
    else:
        print(f"🔄 Юзер {instance.username} оновив свої дані.")

Що ви очікуєте побачити? Коли ви зайдете в адмінку і створите юзера, в консолі сервера з'явиться наше повідомлення.

Чому це круто? Ми не чіпали код самої моделі User (яка взагалі вбудована в Django). Ми розширили функціонал ззовні!


Приклад 2: Класика жанру — Автоматичний профіль

У реальних проєктах часто є модель User (для авторизації) і модель Profile (для аватарки, біографії тощо). Ми хочемо, щоб коли людина реєструється (User), для неї автоматично створювався порожній Profile.

# models.py
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)

# signals.py
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Питання: А що буде, якщо я не додам перевірку if created:? Відповідь: Кожного разу, коли ви просто змінюєте ім'я користувача і тиснете "Save", код намагатиметься створити ще один профіль для того ж юзера. А оскільки у нас OneToOneField, база даних викине помилку (IntegrityError). Тому created — це ваш найкращий друг.


Приклад 3: Запобігання дії (pre_save)

Припустимо, ми хочемо заборонити реєстрацію користувачів з email-адресами на домені @evil-corp.com. Для цього ідеально підходить pre_save (перед збереженням).

from django.db.models.signals import pre_save
from django.core.exceptions import ValidationError

@receiver(pre_save, sender=User)
def ban_evil_corp(sender, instance, **kwargs):
    if instance.email.endswith('@evil-corp.com'):
        raise ValidationError("Ми не працюємо зі злодіями! 🚫")

Результат: Запис в базу навіть не потрапить. Процес перерветься до того, як SQL-запит буде виконано.


4. 🛠 Практична частина

Час кодити! Відкривайте IDE.

Завдання 1: "Слідчий" Створіть сигнал post_delete для моделі Product (або будь-якої іншої). Коли продукт видаляється, записуйте у файл log.txt рядок: "Товар [назва] було знищено!".

Завдання 2: "Чистоплюй" Використайте pre_save на моделі, яка має текстове поле (наприклад, comment). Зробіть так, щоб перед збереженням текст автоматично очищався від зайвих пробілів на початку і в кінці (strip()), а перша літера ставала великою.

Завдання 3: "Один раз — не..." Напишіть сигнал, який спрацьовує при збереженні User, але... зробіть так, щоб він спрацьовував тільки якщо у юзера в полі is_staff стоїть True. (Підказка: просто додайте if всередину функції).

Завдання 4: Міні-кейс "Зміна статусу" У вас є модель Order з полем status (pending, shipped, delivered). Напишіть сигнал, який відстежує зміну статусу. Умова: Якщо статус змінився саме на "shipped", виведіть в консоль "🚚 Відправляємо посилку!". Підказка: У pre_save ви можете дістати стару версію об'єкта з бази через Order.objects.get(pk=instance.pk) і порівняти її status з новим instance.status.

Завдання із зірочкою (Bug Hunt): Студент написав код сигналу у файлі signals.py, але він не працює. Нічого не відбувається. Код правильний. Чому Django його не бачить? (Відповідь: Файл signals.py треба імпортувати в методі ready() у файлі apps.py, інакше Django просто пройде повз нього).


5. 💡 Мислення як у розробника

Окей, ви навчилися користуватися молотком. Тепер головне — не бити ним по вікнах.

🚫 Типова помилка новачка: Безкінечна петля (Infinite Loop)

Подивіться на цей код. Що з ним не так?

@receiver(post_save, sender=Order)
def update_order(sender, instance, **kwargs):
    instance.total_price = 100
    instance.save()  # 😱 УВАГА!

Аналіз: 1. Order зберігається. 2. Викликається сигнал post_save. 3. Всередині сигналу ми робимо instance.save(). 4. Це знову викликає post_save. 5. ...Рекурсія. Stack Overflow. Сервер падає.

Як думає про: "Якщо мені треба змінити дані в тій самій моделі, я використаю pre_save і просто зміню значення поля. save() викликати не треба, воно саме збережеться далі по ланцюжку."

🕵️‍♂️ Пастка bulk_create

Якщо ви зробите User.objects.bulk_create([user1, user2]), сигнали save НЕ спрацюють! SQL-запит піде напряму в базу для швидкості. Мислення про: "Якщо я використовую масові операції, я не можу покладатися на сигнали. Мені доведеться викликати логіку вручну або переписувати підхід".

🧘 Zen of Python: "Explicit is better than implicit"

Сигнали — це магія. Магія — це неявно. Якщо у вас в проєкті 50 сигналів, ви можете зберегти юзера і запустити ланцюгову реакцію, яка покладе сервер, і ви навіть не зрозумієте, звідки прилетіло. Порада: Використовуйте сигнали тільки для side effects (побічних ефектів), які не критичні для основної логіки, або для розчеплення (decoupling) незалежних додатків. Не будуйте на них бізнес-логіку ядра.


6. 🧩 Підсумок

Отже, що ми маємо в сухому залишку:

  1. Сигнали дозволяють реагувати на події в Django (збереження, видалення).
  2. Вони допомагають розділяти код (Decoupling).
  3. Вони працюють синхронно (блокують виконання).
  4. Ви тепер вмієте автоматично створювати профілі, валідувати дані та "шпигувати" за подіями.

🤔 Тизер наступного уроку

Ми згадали, що сигнали синхронні. Але що, якщо відправка вітального email займає 5 секунд? Невже користувач має дивитися на білий екран і чекати? Це неприпустимо! На наступному уроці ми дізнаємося, як викинути важкі задачі з основного потоку у фоновий режим. Готуйтеся, тема: Celery та асинхронність!

А поки що — це був CS50. Щасти вам з кодом!