refactor(calendar_app): реструктуризация notifications в модульную архитектуру
Проблема: - Монолитный notification_signals.py (~220 строк) смешивал разные concerns - Логика отслеживания изменений полей переплетена с signal receivers - Вспомогательные функции без четкой структуры Решение: Разделен на модульную структуру calendar_app/notifications/: - config.py - NotificationVerbs, MessageTemplates, ActionURLs, константы полей, вспомогательные функции (format_date, format_changes, FIELD_NAMES, IMPORTANT_FIELDS) - handlers.py - get_event_recipients(), notify_event_created(), notify_event_changed(), notify_event_cancelled() - signals.py - Django signal receivers (@receiver) для CalendarEvent - __init__.py - публичный API модуля Изменения: - Создан: calendar_app/notifications/ модуль с 4 файлами - Переименован: notification_signals.py → notification_signals.py.old - Изменен: apps.py - обновлен импорт на calendar_app.notifications Логика уведомлений: 1. Новое событие → получатели по типу (компания/отдел/личное) 2. Изменение события → участники события (если изменены важные поля) 3. Отмена события → все участники Преимущества: - ✅ Единообразие со всеми остальными приложениями - ✅ Четкое разделение ответственности (SRP) - ✅ Отслеживание изменений полей вынесено в отдельный signal - ✅ Централизованные константы для важных полей - ✅ Легче тестировать компоненты - ✅ Публичный API для переиспользования Верификация: - python manage.py check - успешно
This commit is contained in:
parent
d9a8e71e3a
commit
fc9b9de50a
@ -17,7 +17,7 @@ class CalendarAppConfig(AppConfig):
|
||||
Raises:
|
||||
LookupError: Если приложение 'employees' или модель 'Employee' не найдены.
|
||||
"""
|
||||
import calendar_app.notification_signals # noqa: F401
|
||||
import calendar_app.notifications # Модуль уведомлений (signals регистрируются автоматически)
|
||||
from calendar_app import signals # noqa: F401 - старая система
|
||||
import calendar_app.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
|
||||
37
backend/calendar_app/notifications/__init__.py
Normal file
37
backend/calendar_app/notifications/__init__.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
Модуль уведомлений для Calendar приложения.
|
||||
|
||||
Публичный API:
|
||||
- NotificationVerbs - константы глаголов уведомлений
|
||||
- MessageTemplates - шаблоны сообщений
|
||||
- ActionURLs - генераторы URL-адресов
|
||||
- get_event_recipients - получение списка получателей для события
|
||||
|
||||
Использование:
|
||||
from calendar_app.notifications import get_event_recipients
|
||||
|
||||
# В коде:
|
||||
recipients = get_event_recipients(event)
|
||||
"""
|
||||
|
||||
from .config import (
|
||||
NotificationVerbs,
|
||||
MessageTemplates,
|
||||
ActionURLs,
|
||||
format_date,
|
||||
format_changes,
|
||||
)
|
||||
from .handlers import get_event_recipients
|
||||
|
||||
# Импортируем handlers и signals для регистрации
|
||||
from . import handlers # noqa: F401
|
||||
from . import signals # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
'NotificationVerbs',
|
||||
'MessageTemplates',
|
||||
'ActionURLs',
|
||||
'get_event_recipients',
|
||||
'format_date',
|
||||
'format_changes',
|
||||
]
|
||||
126
backend/calendar_app/notifications/config.py
Normal file
126
backend/calendar_app/notifications/config.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""
|
||||
Конфигурация уведомлений для модуля Calendar.
|
||||
|
||||
Содержит:
|
||||
- NotificationVerbs - глаголы для уведомлений
|
||||
- MessageTemplates - шаблоны сообщений
|
||||
- ActionURLs - URL-адреса действий
|
||||
- Вспомогательные функции
|
||||
"""
|
||||
|
||||
|
||||
class NotificationVerbs:
|
||||
"""Глаголы (verbs) для уведомлений calendar."""
|
||||
|
||||
EVENT_CREATED = 'event_created'
|
||||
EVENT_CHANGED = 'event_changed'
|
||||
EVENT_CANCELLED = 'event_cancelled'
|
||||
EVENT_REMINDER = 'event_reminder' # Для будущей реализации через Celery
|
||||
|
||||
|
||||
class MessageTemplates:
|
||||
"""Шаблоны сообщений для уведомлений calendar."""
|
||||
|
||||
@staticmethod
|
||||
def event_created_company(event_title: str, event_date: str) -> str:
|
||||
"""Шаблон для нового события компании."""
|
||||
return f'Добавлено событие компании: "{event_title}" ({event_date})'
|
||||
|
||||
@staticmethod
|
||||
def event_created_department(
|
||||
department_name: str,
|
||||
event_title: str,
|
||||
event_date: str
|
||||
) -> str:
|
||||
"""Шаблон для нового события отдела."""
|
||||
return (
|
||||
f'Добавлено событие отдела {department_name}: '
|
||||
f'"{event_title}" ({event_date})'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def event_created_title() -> str:
|
||||
"""Заголовок для нового события."""
|
||||
return 'Новое событие в календаре'
|
||||
|
||||
@staticmethod
|
||||
def event_changed(event_title: str, changes_text: str) -> str:
|
||||
"""Шаблон для изменения события."""
|
||||
return f'Событие "{event_title}" изменено. Изменения: {changes_text}'
|
||||
|
||||
@staticmethod
|
||||
def event_changed_title() -> str:
|
||||
"""Заголовок для изменения события."""
|
||||
return 'Событие изменено'
|
||||
|
||||
@staticmethod
|
||||
def event_cancelled(event_title: str, event_date: str) -> str:
|
||||
"""Шаблон для отмены события."""
|
||||
return f'Событие "{event_title}" ({event_date}) отменено'
|
||||
|
||||
@staticmethod
|
||||
def event_cancelled_title() -> str:
|
||||
"""Заголовок для отмены события."""
|
||||
return 'Событие отменено'
|
||||
|
||||
|
||||
class ActionURLs:
|
||||
"""URL-адреса для действий с календарем."""
|
||||
|
||||
CALENDAR = '/calendar'
|
||||
|
||||
@staticmethod
|
||||
def event_detail(event_id: int) -> str:
|
||||
"""
|
||||
Возвращает URL-адрес события.
|
||||
|
||||
TODO: реализовать прямые ссылки на конкретные события
|
||||
"""
|
||||
return f'/calendar?event={event_id}'
|
||||
|
||||
|
||||
# ===== Константы для полей =====
|
||||
|
||||
FIELD_NAMES = {
|
||||
'title': 'название',
|
||||
'start_date': 'дата начала',
|
||||
'end_date': 'дата окончания',
|
||||
'start_time': 'время начала',
|
||||
'end_time': 'время окончания',
|
||||
'location': 'место проведения',
|
||||
}
|
||||
|
||||
IMPORTANT_FIELDS = [
|
||||
'title', 'start_date', 'end_date',
|
||||
'start_time', 'end_time', 'location'
|
||||
]
|
||||
|
||||
|
||||
def format_date(date) -> str:
|
||||
"""
|
||||
Форматирует дату в строку.
|
||||
|
||||
Args:
|
||||
date: Объект date или datetime
|
||||
|
||||
Returns:
|
||||
str: Отформатированная дата (например, "10.03.2026")
|
||||
"""
|
||||
if not date:
|
||||
return ''
|
||||
return date.strftime('%d.%m.%Y')
|
||||
|
||||
|
||||
def format_changes(changed_fields: list[str]) -> str:
|
||||
"""
|
||||
Форматирует список изменённых полей в текст.
|
||||
|
||||
Args:
|
||||
changed_fields: Список имён полей
|
||||
|
||||
Returns:
|
||||
str: Текстовое описание изменений через запятую
|
||||
"""
|
||||
return ', '.join([
|
||||
FIELD_NAMES.get(f, f) for f in changed_fields
|
||||
])
|
||||
174
backend/calendar_app/notifications/handlers.py
Normal file
174
backend/calendar_app/notifications/handlers.py
Normal file
@ -0,0 +1,174 @@
|
||||
"""
|
||||
Бизнес-логика отправки уведомлений для модуля Calendar.
|
||||
|
||||
Функции:
|
||||
- get_event_recipients - определение получателей уведомлений
|
||||
- notify_event_created - уведомление о новом событии
|
||||
- notify_event_changed - уведомление об изменении события
|
||||
- notify_event_cancelled - уведомление об отмене события
|
||||
"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from notifications.signals import notify
|
||||
|
||||
from .config import (
|
||||
NotificationVerbs,
|
||||
MessageTemplates,
|
||||
ActionURLs,
|
||||
format_date,
|
||||
format_changes,
|
||||
)
|
||||
|
||||
Employee = get_user_model()
|
||||
|
||||
|
||||
def get_event_recipients(event) -> list:
|
||||
"""
|
||||
Возвращает список получателей уведомлений для события.
|
||||
|
||||
Args:
|
||||
event: Объект CalendarEvent
|
||||
|
||||
Returns:
|
||||
list: Список пользователей (Employee):
|
||||
- Для личного события - только владелец
|
||||
- Для события компании - все активные сотрудники
|
||||
- Для события отдела - сотрудники этого отдела
|
||||
"""
|
||||
if event.is_personal:
|
||||
# Личное событие - только владелец
|
||||
return [event.employee] if event.employee else []
|
||||
|
||||
elif event.is_company:
|
||||
# Событие компании - все активные сотрудники
|
||||
return list(Employee.objects.filter(is_active=True))
|
||||
|
||||
elif event.department:
|
||||
# Событие отдела - сотрудники через EmployeeDepartment
|
||||
from employees.models import EmployeeDepartment
|
||||
employee_ids = EmployeeDepartment.objects.filter(
|
||||
department=event.department,
|
||||
is_active=True
|
||||
).values_list('employee_id', flat=True)
|
||||
return list(Employee.objects.filter(id__in=employee_ids, is_active=True))
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def notify_event_created(event):
|
||||
"""
|
||||
Отправляет уведомления о новом событии всем участникам.
|
||||
|
||||
Args:
|
||||
event: Объект CalendarEvent
|
||||
"""
|
||||
recipients = get_event_recipients(event)
|
||||
|
||||
# Исключаем создателя события
|
||||
if event.created_by:
|
||||
recipients = [r for r in recipients if r.id != event.created_by.id]
|
||||
|
||||
# Определяем тип события для сообщения
|
||||
event_date = format_date(event.start_date)
|
||||
|
||||
if event.is_company:
|
||||
description = MessageTemplates.event_created_company(event.title, event_date)
|
||||
elif event.department:
|
||||
description = MessageTemplates.event_created_department(
|
||||
event.department.name,
|
||||
event.title,
|
||||
event_date
|
||||
)
|
||||
else:
|
||||
# Личное событие - не должно попадать сюда (нет других получателей)
|
||||
return
|
||||
|
||||
# Отправляем уведомления
|
||||
for recipient in recipients:
|
||||
notify.send(
|
||||
sender=event.created_by,
|
||||
recipient=recipient,
|
||||
verb=NotificationVerbs.EVENT_CREATED,
|
||||
action_object=event,
|
||||
description=description,
|
||||
action_url=ActionURLs.CALENDAR,
|
||||
data={
|
||||
'title': MessageTemplates.event_created_title(),
|
||||
'event_id': event.id,
|
||||
'event_title': event.title,
|
||||
'event_date': event.start_date.isoformat(),
|
||||
'department_id': (
|
||||
event.department.id if event.department else None
|
||||
),
|
||||
'is_company_event': event.is_company,
|
||||
'created_by_id': (
|
||||
event.created_by.id if event.created_by else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def notify_event_changed(event, changed_fields: list[str]):
|
||||
"""
|
||||
Отправляет уведомления об изменении события.
|
||||
|
||||
Args:
|
||||
event: Объект CalendarEvent
|
||||
changed_fields: Список изменённых полей
|
||||
"""
|
||||
if not changed_fields:
|
||||
return
|
||||
|
||||
recipients = get_event_recipients(event)
|
||||
|
||||
# Формируем сообщение об изменениях
|
||||
changes_text = format_changes(changed_fields)
|
||||
|
||||
for recipient in recipients:
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=recipient,
|
||||
verb=NotificationVerbs.EVENT_CHANGED,
|
||||
action_object=event,
|
||||
description=MessageTemplates.event_changed(event.title, changes_text),
|
||||
action_url=ActionURLs.CALENDAR,
|
||||
data={
|
||||
'title': MessageTemplates.event_changed_title(),
|
||||
'event_id': event.id,
|
||||
'event_title': event.title,
|
||||
'changed_fields': changed_fields,
|
||||
'event_date': event.start_date.isoformat(),
|
||||
'department_id': (
|
||||
event.department.id if event.department else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def notify_event_cancelled(event):
|
||||
"""
|
||||
Отправляет уведомления об отмене (удалении) события.
|
||||
|
||||
Args:
|
||||
event: Объект CalendarEvent
|
||||
"""
|
||||
recipients = get_event_recipients(event)
|
||||
event_date = format_date(event.start_date)
|
||||
|
||||
for recipient in recipients:
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=recipient,
|
||||
verb=NotificationVerbs.EVENT_CANCELLED,
|
||||
description=MessageTemplates.event_cancelled(event.title, event_date),
|
||||
action_url=ActionURLs.CALENDAR,
|
||||
data={
|
||||
'title': MessageTemplates.event_cancelled_title(),
|
||||
'event_title': event.title,
|
||||
'event_date': event.start_date.isoformat(),
|
||||
'department_id': (
|
||||
event.department.id if event.department else None
|
||||
),
|
||||
'is_company_event': event.is_company,
|
||||
}
|
||||
)
|
||||
75
backend/calendar_app/notifications/signals.py
Normal file
75
backend/calendar_app/notifications/signals.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""
|
||||
Django signals для автоматической генерации уведомлений в модуле Calendar.
|
||||
|
||||
Обрабатывает события:
|
||||
- post_save для CalendarEvent - создание или изменение события
|
||||
- pre_save для CalendarEvent - отслеживание изменений полей
|
||||
- pre_delete для CalendarEvent - отмена (удаление) события
|
||||
"""
|
||||
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from ..models import CalendarEvent
|
||||
from .config import IMPORTANT_FIELDS
|
||||
from .handlers import (
|
||||
notify_event_created,
|
||||
notify_event_changed,
|
||||
notify_event_cancelled,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=CalendarEvent)
|
||||
def create_event_notifications(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Создает уведомления при создании или изменении события.
|
||||
|
||||
Уведомления отправляются:
|
||||
- При создании - всем сотрудникам компании или отдела
|
||||
- При изменении - участникам события (если изменились важные поля)
|
||||
|
||||
Уведомления отправляются через универсальную систему
|
||||
channels.py → Celery → WebSocket/Email/Push
|
||||
"""
|
||||
if created:
|
||||
notify_event_created(instance)
|
||||
else:
|
||||
changed_fields = getattr(instance, '_changed_fields', [])
|
||||
if changed_fields:
|
||||
notify_event_changed(instance, changed_fields)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=CalendarEvent)
|
||||
def track_event_changes(sender, instance, **kwargs):
|
||||
"""
|
||||
Отслеживает изменения важных полей события для уведомлений.
|
||||
|
||||
Сохраняет список изменённых полей в атрибут _changed_fields,
|
||||
который затем используется в post_save signal.
|
||||
"""
|
||||
if instance.pk:
|
||||
try:
|
||||
old_event = CalendarEvent.objects.get(pk=instance.pk)
|
||||
changed_fields = []
|
||||
|
||||
# Проверяем изменения важных полей
|
||||
for field in IMPORTANT_FIELDS:
|
||||
old_value = getattr(old_event, field)
|
||||
new_value = getattr(instance, field)
|
||||
if old_value != new_value:
|
||||
changed_fields.append(field)
|
||||
|
||||
instance._changed_fields = changed_fields
|
||||
except CalendarEvent.DoesNotExist:
|
||||
instance._changed_fields = []
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=CalendarEvent)
|
||||
def notify_event_cancelled_signal(sender, instance, **kwargs):
|
||||
"""
|
||||
Создает уведомления при удалении (отмене) события.
|
||||
|
||||
Уведомления отправляются через универсальную систему
|
||||
channels.py → Celery → WebSocket/Email/Push
|
||||
"""
|
||||
notify_event_cancelled(instance)
|
||||
Loading…
x
Reference in New Issue
Block a user