🏠 Домашнее задание 21 — Урок 45
⚡ Суть задания
Реализовать сигналы Django для задач (Task): при смене статуса или закрытии задачи — отправить email-уведомление владельцу задачи через ConsoleBackend. Добавить защиту от повторной отправки при последовательных изменениях.
- Сигнал:
post_saveна модель Task - Условие: статус изменился ИЛИ задача закрыта
- Email-бэкенд: ConsoleBackend (вывод в консоль)
- Защита от дублей: хранить предыдущий статус в
__init__или черезpre_save
Задание из LMS (Python Advanced: Домашнее задание 21)
Реализуйте сигналы на закрытие и перевода статусов у главных задач. Если задача переходит в новый статус, или закрывается — тот, кому принадлежит эта задача должен получать Email уведомление на свою почту.
Используйте Базовый тестовый Django сервер, который будет выводить шаблон сообщения в консоль.
Добавить проверку, чтобы уведомление не отправлялось повторно при последовательных изменения статуса, если это нежелательно.
1. Подготовка окружения
1.1 Активация виртуального окружения
# Windows PowerShell
cd your_project
python -m venv venv
venv\Scripts\Activate.ps1
# Установить зависимости (если requirements.txt есть)
pip install django djangorestframework
1.2 Убедиться, что проект работает
python manage.py check
python manage.py migrate
1.3 Структура файлов для задания
Предполагаем, что есть приложение tasks с моделью Task. Нам нужно:
tasks/
__init__.py
apps.py ← изменить: добавить ready()
models.py ← изменить: добавить поле status, is_closed, owner
signals.py ← создать: обработчик сигнала
views.py
...
settings.py ← изменить: добавить EMAIL_BACKEND
2. Шаг 1 — Модель Task
Связь с теорией: модель — источник событий, её save() запускает сигналы.
# tasks/models.py
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
STATUS_CHOICES = [
('new', 'Новая'),
('in_progress', 'В работе'),
('done', 'Выполнена'),
('closed', 'Закрыта'),
]
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='new')
is_closed = models.BooleanField(default=False)
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='tasks',
null=True,
blank=True,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f'Task #{self.pk}: {self.title} [{self.status}]'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Сохраняем начальный статус для сравнения в сигнале
self._original_status = self.status
self._original_is_closed = self.is_closed
Логика __init__: при загрузке объекта из БД запоминаем текущий статус в атрибутах экземпляра. После вызова .save() в обработчике post_save можно сравнить instance.status и instance._original_status, чтобы понять — изменился ли статус.
3. Шаг 2 — Настройка EMAIL_BACKEND
Связь с теорией (email) и примером 3.
# settings.py — добавить строку
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
ConsoleBackend — тестовый бэкенд. Все письма будут выводиться в консоль сервера, а не отправляться по-настоящему. Это означает: при запуске manage.py runserver в терминале вы увидите полный текст письма с заголовками.
4. Шаг 3 — Обработчик сигнала (signals.py)
Связь с теорией (обработчики) и примером 3.
# tasks/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 Task
@receiver(post_save, sender=Task)
def notify_owner_on_status_change(sender, instance, created, **kwargs):
"""
Отправляет email владельцу задачи при:
- создании задачи (created=True)
- изменении статуса
- закрытии задачи (is_closed=True)
Защита от дублей: сравниваем текущий статус с _original_status.
"""
# Проверяем, есть ли владелец и его email
if not instance.owner or not instance.owner.email:
return # Некому отправлять
status_changed = instance.status != instance._original_status
just_closed = instance.is_closed and not instance._original_is_closed
# Отправляем только если что-то реально изменилось
if not created and not status_changed and not just_closed:
return # Статус не изменился, уведомление не нужно
# Формируем тему и тело письма
if just_closed:
subject = f'Task #{instance.pk} closed'
message = (
f'Your task "{instance.title}" has been closed.\n'
f'Final status: {instance.get_status_display()}'
)
elif created:
subject = f'Task #{instance.pk} created'
message = (
f'Your task "{instance.title}" has been created.\n'
f'Status: {instance.get_status_display()}'
)
else:
subject = f'Task #{instance.pk} status changed'
message = (
f'Task "{instance.title}" status changed:\n'
f'{instance._original_status} → {instance.status} '
f'({instance.get_status_display()})'
)
send_mail(
subject=subject,
message=message,
from_email='noreply@taskmanager.com',
recipient_list=[instance.owner.email],
fail_silently=True, # Не ронять сервер при ошибке email
)
# Обновляем _original_status после отправки
# (чтобы следующий save() снова мог сравнивать)
instance._original_status = instance.status
instance._original_is_closed = instance.is_closed
5. Шаг 4 — Регистрация сигнала в apps.py
Связь с теорией (apps.py).
# tasks/apps.py
from django.apps import AppConfig
class TasksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tasks'
def ready(self):
import tasks.signals # Регистрируем обработчики сигналов
Убедитесь, что в settings.py → INSTALLED_APPS приложение указано как 'tasks' (Django 3.2+ найдёт TasksConfig автоматически) или явно 'tasks.apps.TasksConfig'.
6. Шаг 5 — Создание и применение миграций
python manage.py makemigrations tasks
python manage.py migrate
7. Шаг 6 — Проверка в консоли
# Запускаем сервер разработки
python manage.py runserver
В другом терминале открываем Django shell:
python manage.py shell
# В Django shell
from django.contrib.auth.models import User
from tasks.models import Task
# Создаём пользователя с email
user = User.objects.create_user('testuser', 'test@example.com', 'password')
# Создаём задачу — должен появиться email в консоли сервера
task = Task.objects.create(title='Test Task', owner=user)
# Меняем статус — должен появиться email с изменением
task.status = 'in_progress'
task.save()
# Снова меняем статус
task.status = 'done'
task.save()
# Закрываем задачу
task.is_closed = True
task.save()
# Сохраняем без изменений — уведомление НЕ должно отправиться
task.save() # _original_status == status → return
Ожидаемый вывод в консоли runserver при создании задачи:
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Task #1 created
From: noreply@taskmanager.com
To: test@example.com
Date: ...
Your task "Test Task" has been created.
Status: Новая
При изменении статуса:
Subject: Task #1 status changed
...
Task "Test Task" status changed:
new → in_progress (В работе)
8. Проверка в VS Code
8.1 Запуск через терминал
- Откройте проект в VS Code
- Ctrl+` — открыть встроенный терминал
- Активируйте venv:
venv\Scripts\Activate.ps1(Windows) - Запустите сервер:
python manage.py runserver - В новом терминале (Ctrl+Shift+5):
python manage.py shell - Выполняйте команды из шага 6 — наблюдайте email в первом терминале
8.2 Отладка через F5 (launch.json)
Создайте файл .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver", "--noreload"],
"django": true,
"justMyCode": true
}
]
}
Параметр --noreload важен при отладке — без него Django перезапускает процесс при изменении файлов, что мешает точкам останова.
8.3 Точки останова в signals.py
- Откройте
tasks/signals.py - Кликните в левое поле строки с
status_changed = ...— красная точка (breakpoint) - Нажмите F5 — запустится Django с отладчиком
- В другом терминале:
python manage.py shell→ создайте Task - VS Code остановится на точке останова — в панели Variables увидите
instance,created,status_changed - F10 — шаг вперёд, F11 — войти в функцию, F5 — продолжить до следующей точки
9. Связь с материалами урока
| Элемент задания | Где в уроке |
|---|---|
| Декоратор @receiver(post_save) | Теория: обработчики сигналов |
| AppConfig.ready() для регистрации | Теория: регистрация в apps.py |
| EMAIL_BACKEND = console | Теория: настройка email |
| Параметр created | Теория: параметры post_save |
| send_mail() | Примеры: уведомление администратора |
| Защита от повтора через _original_status | Старый vs Новый: защита от raw |
| Двойная отправка при ошибке регистрации | Ошибки: дублирование обработчика |
10. Критерии выполнения
- При создании задачи в консоли сервера появляется email с темой «Task #N created»
- При смене статуса — email с темой «Task #N status changed» и обоими статусами
- При закрытии (is_closed=True) — email с темой «Task #N closed»
- При повторном
.save()без изменения статуса — email НЕ отправляется - Сигналы зарегистрированы в
apps.py ready(), а не вmodels.py EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'в settings.py