⚖️ Старый vs Новый — Урок 45

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

⚡ Ключевые изменения

  • Регистрация сигналов в models.py → правильно в apps.py ready()
  • Пароль SMTP в коде напрямую → переменные окружения (os.environ.get())
  • Только send_mail() → также EmailMessage с вложениями, cc, bcc
  • Метод .connect() в globals → @receiver декоратор в signals.py

1. Место регистрации сигналов

Из лекции (старое)

# models.py — ПЛОХО
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Book)
def book_saved(sender, instance, created, **kwargs):
    ...

# Или в конце models.py:
post_save.connect(book_saved, sender=Book)

Проблема: при импорте models.py происходит регистрация сигнала. Если модуль импортируется несколько раз — обработчик подключается несколько раз, письмо/логика выполняется дважды.

Современное (Django 5.x)

# signals.py — отдельный модуль
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Book

@receiver(post_save, sender=Book)
def book_saved(sender, instance, created, **kwargs):
    ...

# apps.py — регистрация при старте
from django.apps import AppConfig

class BookConfig(AppConfig):
    name = 'books'

    def ready(self):
        import books.signals  # Один импорт при старте Django

Преимущество: ready() вызывается ровно один раз при полной загрузке всех приложений. Сигналы зарегистрированы безопасно, без дублирования.

2. Хранение пароля EMAIL_HOST_PASSWORD

Из лекции (небезопасно)

# settings.py
EMAIL_HOST_PASSWORD = 'your_password'

Проблема: пароль в открытом виде в коде → попадает в git-репозиторий → утечка учётных данных.

Современное (Django 5.x)

# settings.py
import os
EMAIL_HOST_PASSWORD = os.environ.get(
    'EMAIL_HOST_PASSWORD', ''
)

# Или через python-decouple:
from decouple import config
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')

Преимущество: пароль хранится в переменных окружения или файле .env (добавлен в .gitignore).

3. Регистрация AppConfig (default_app_config)

Устаревший способ (Django < 3.2)

# books/__init__.py
default_app_config = 'books.apps.BookConfig'

Проблема: устаревший механизм, удалён в Django 4.x.

Современное (Django 3.2+)

# settings.py
INSTALLED_APPS = [
    'books.apps.BookConfig',  # Указываем явно
    # или краткое имя если default_auto_field настроен:
    'books',
]

Преимущество: явная регистрация AppConfig в INSTALLED_APPS. Django 3.2+ автоматически ищет AppConfig без __init__.py.

4. Отправка email только при создании (не при обновлении)

Без проверки created (плохо)

@receiver(post_save, sender=Book)
def notify_admin(sender, instance, **kwargs):
    # Отправляет при каждом save()!
    send_mail(
        'Book changed',
        f'Book {instance.id} changed.',
        'admin@example.com',
        ['admin@example.com'],
    )

Проблема: письмо отправляется при каждом вызове .save(), в том числе при обновлении.

С проверкой created (правильно)

@receiver(post_save, sender=Book)
def notify_admin(sender, instance, created, **kwargs):
    if created:  # Только при первичном создании
        send_mail(
            'New Book Created',
            f'Book {instance.id} has been created.',
            'admin@example.com',
            ['admin@example.com'],
        )

Преимущество: чёткий контроль — письмо отправляется только при создании нового объекта.

5. Защита от raw=True (загрузка фикстур)

Без проверки raw

@receiver(post_save, sender=User)
def create_token(sender, instance, created, **kwargs):
    if created:
        Token.objects.create(user=instance)
# При loaddata fixtures — создаёт токены для тестовых данных

С проверкой raw

@receiver(post_save, sender=User)
def create_token(sender, instance, created, raw, **kwargs):
    if created and not raw:
        # raw=True при loaddata — пропускаем
        Token.objects.create(user=instance)

Преимущество: при загрузке фикстур (manage.py loaddata) токены не создаются повторно для уже существующих пользователей.