🐛 Типичные ошибки — Урок 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 (пароль приложения).
Решение:
- Включите двухфакторную аутентификацию в Google-аккаунте
- Создайте App Password: Google Account → Security → App Passwords
- Используйте сгенерированный пароль (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'