💻 Примеры — Урок 45

← К оглавлению урока

⚡ Ключевые примеры

  • Пример 1: post_save → автоматически создать токен при регистрации пользователя
  • Пример 2: pre_save → обновить updated_at перед каждым сохранением
  • Пример 3: post_save → отправить email администратору при создании объекта
  • Пример 4: post_delete → залогировать удаление объекта

Пример 1: Автоматическое создание токена для нового пользователя

При регистрации нового пользователя автоматически создаётся токен для аутентификации через DRF TokenAuthentication.

# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

Разбор:

  • @receiver(post_save, sender=User) — регистрируем обработчик для сигнала post_save модели User
  • created=False — значение по умолчанию в сигнатуре функции; реальное значение передаёт Django
  • Проверка if created — токен создаётся только при первом создании пользователя, не при обновлении
  • Token.objects.create(user=instance) — создаём токен, связанный с конкретным пользователем
# apps.py
from django.apps import AppConfig

class UsersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'users'

    def ready(self):
        import users.signals  # Активируем обработчики сигналов

Пример 2: Обновление поля updated_at перед сохранением

При каждом сохранении объекта Book автоматически обновляется поле updated_at. Используется сигнал pre_save, чтобы изменить экземпляр ещё до записи в БД.

Модель (models.py)

# models.py
from django.db import models
from django.contrib.auth.models import User

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published_date = models.DateField()
    created_at = models.DateTimeField(null=True, blank=True)
    updated_at = models.DateTimeField(null=True, blank=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books')

Обработчик через connect() (signals.py)

# signals.py
from django.utils import timezone
from django.db.models.signals import pre_save
from .models import Book

def update_timestamp(sender, instance, **kwargs):
    instance.updated_at = timezone.now()

# Подключение функции-обработчика к сигналу
pre_save.connect(update_timestamp, sender=Book)

Разбор:

  • Сигнал pre_save срабатывает до сохранения — изменение instance.updated_at сразу попадёт в запись БД
  • Метод pre_save.connect(update_timestamp, sender=Book) регистрирует обработчик без декоратора
  • timezone.now() возвращает timezone-aware datetime — правильно для Django с USE_TZ = True

Пример 3: Email-уведомление администратора при создании объекта

Когда создаётся новый объект Book, на почту администратора отправляется уведомление.

Настройки (settings.py)

# settings.py — для тестирования (письма в консоль)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Обработчик (signals.py)

# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.mail import send_mail
from .models import Book

@receiver(post_save, sender=Book)
def notify_admin_on_new_book(sender, instance, created, **kwargs):
    if created:
        send_mail(
            subject='New Book Created',
            message=f'Book {instance.id} "{instance.title}" has been created.',
            from_email='admin@gmail.com',
            recipient_list=['admin@gmail.com'],
        )

Разбор:

  • Условие if created — письмо отправляется только при первичном создании, не при каждом save()
  • send_mail() — стандартная Django-функция; с ConsoleBackend выведет письмо в консоль вместо реальной отправки
  • instance.id и instance.title — доступны, поскольку обработчик post_save вызывается уже после записи в БД

Ожидаемый вывод в консоли

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: New Book Created
From: admin@gmail.com
To: admin@gmail.com
Date: Tue, 16 Jun 2024 00:03:08 -0000
Message-ID: <171928458886.12044.11920313322863035917@DESKTOP-CI123U1>

Book 60 "Django for Beginners" has been created.

Пример 4: Логирование удаления объектов

При удалении объекта Genre информация об этом фиксируется в логах приложения.

# signals.py
import logging
from django.db.models.signals import post_delete
from django.dispatch import receiver
from .models import Genre

logger = logging.getLogger(__name__)

@receiver(post_delete, sender=Genre)
def log_genre_deletion(sender, instance, **kwargs):
    logger.info(f'Genre deleted: {instance.name}')

Разбор:

  • logging.getLogger(__name__) — создаёт логгер с именем текущего модуля (например, first_app.signals)
  • post_delete — срабатывает после удаления; instance ещё доступен (хотя pk уже None в некоторых версиях)
  • logger.info() — запись в лог; настройте LOGGING в settings.py для сохранения в файл

Пример 5: Полный поток — создание объекта и сигнал

Демонстрация полного цикла: создание объекта → автоматический сигнал → вызов обработчика.

# В Django shell или views.py
from first_app.models import Book

# Создание объекта запускает сигнал post_save с created=True
book = Book.objects.create(
    title="New Book",
    author="Author Name",
    published_date="2000-01-01",
    owner_id=1
)
# → Автоматически вызывается notify_admin_on_new_book()
# → В консоли появляется mock-email

# Обновление объекта запускает post_save с created=False
book.title = "Updated Title"
book.save()
# → Обработчик вызывается, но условие if created пропускает send_mail()

Пример 6: Настройка реального SMTP (Gmail)

Для Gmail необходимо использовать App Password (пароль приложения), а не обычный пароль аккаунта. Двухфакторная аутентификация должна быть включена.
# settings.py — настройка для Gmail SMTP
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your_email@gmail.com'
EMAIL_HOST_PASSWORD = 'abcd efgh ijkl mnop'  # App Password из Google Account

# Лучше — через переменные окружения:
import os
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
⚠️ Проверить по документации: настройки Gmail для App Passwords и OAuth2 могут меняться. Актуальные инструкции см. в справке Google.