🐛 Типичные ошибки — Урок 45

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

⚡ Топ-3 ошибки

  • Сигнал не срабатывает — не импортирован signals.py в apps.py ready()
  • Email отправляется при каждом save() — нет проверки if created
  • Циклический импорт — signals.py импортируется в models.py

Разбор типичных ошибок

🐛 Ошибка 1: Сигнал не срабатывает

Симптом: написали обработчик в signals.py, создали объект — ничего не происходит.

# signals.py (написан, но нигде не импортирован)
@receiver(post_save, sender=Book)
def book_saved(sender, instance, created, **kwargs):
    print("Book saved!")  # Никогда не вызывается

Причина: модуль signals.py не импортирован при старте Django, поэтому декоратор @receiver никогда не выполняется и обработчик не регистрируется.

# apps.py — РЕШЕНИЕ: импортировать в ready()
class BookConfig(AppConfig):
    name = 'books'

    def ready(self):
        import books.signals  # Теперь декоратор @receiver выполнится

Проверка: убедитесь также, что в INSTALLED_APPS указан 'books.apps.BookConfig' или просто 'books' (Django 3.2+ найдёт AppConfig автоматически).

🐛 Ошибка 2: Email отправляется при каждом .save()

Симптом: email уходит не только при создании, но и при каждом обновлении объекта — это спам-поведение.

# ПЛОХО: нет проверки created
@receiver(post_save, sender=Book)
def notify_admin(sender, instance, **kwargs):
    send_mail('Book changed', f'Book {instance.id}', 'a@a.com', ['a@a.com'])
# ХОРОШО: проверка created
@receiver(post_save, sender=Book)
def notify_admin(sender, instance, created, **kwargs):
    if created:  # Только при первичном создании
        send_mail('New Book', f'Book {instance.id} created', 'a@a.com', ['a@a.com'])

🐛 Ошибка 3: Циклический импорт

Симптом: при запуске Django — ImportError: cannot import name 'Book' from 'books.models' или аналогичная ошибка.

# models.py — ПЛОХО
from django.db import models
import books.signals  # Импорт signals в models.py вызывает цикл

class Book(models.Model): ...
# signals.py
from .models import Book  # Пытается импортировать из models.py,
                           # который сам импортирует signals.py → цикл

Решение: никогда не импортировать signals.py из models.py. Правильное место — исключительно apps.py → ready().

🐛 Ошибка 4: Двойная регистрация обработчика

Симптом: обработчик вызывается дважды (или более) при одном событии — например, два одинаковых email.

Причина: сигнал зарегистрирован в нескольких местах (например, и в models.py, и в apps.py ready()), или ready() вызывается повторно.

# Диагностика: добавить dispatch_uid для уникальности
@receiver(post_save, sender=Book, dispatch_uid="books.signals.book_saved")
def book_saved(sender, instance, created, **kwargs):
    ...

Параметр dispatch_uid гарантирует, что обработчик с данным uid регистрируется только один раз.

🐛 Ошибка 5: SMTPAuthenticationError при отправке

Симптом: smtplib.SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted') при использовании Gmail.

Причина: обычный пароль аккаунта Google не принимается. Gmail требует App Password (пароль приложения).

Решение:

  1. Включите двухфакторную аутентификацию в Google-аккаунте
  2. Создайте App Password: Google Account → Security → App Passwords
  3. Используйте сгенерированный пароль (16 символов) в EMAIL_HOST_PASSWORD
# settings.py
EMAIL_HOST_PASSWORD = os.environ.get('GMAIL_APP_PASSWORD')

🐛 Ошибка 6: Попытка изменить объект в post_save через .save() — рекурсия

Симптом: RecursionError: maximum recursion depth exceeded или бесконечный цикл сохранений.

# ПЛОХО: вызов save() внутри post_save → новый post_save → ...
@receiver(post_save, sender=Book)
def update_book(sender, instance, **kwargs):
    instance.some_field = 'updated'
    instance.save()  # Рекурсия!
# ХОРОШО: использовать update() или update_fields
@receiver(post_save, sender=Book)
def update_book(sender, instance, created, **kwargs):
    if created:
        # Вариант 1: .update() не вызывает сигналы
        Book.objects.filter(pk=instance.pk).update(some_field='updated')
        # Вариант 2: сохранить с ограниченным набором полей
        # (всё равно рекурсивно! Лучше использовать pre_save или .update())

🐛 Ошибка 7: EMAIL_BACKEND не настроен — ConnectionRefusedError

Симптом: ConnectionRefusedError: [Errno 111] Connection refused при вызове send_mail().

Причина: EMAIL_BACKEND не указан в settings.py. По умолчанию Django использует smtp.EmailBackend и пытается подключиться к localhost:25.

# settings.py — добавить хотя бы для разработки:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'