Проблема: - Монолитный notification_signals.py (~520 строк) смешивал разные concerns - Сложная логика с множественными сигналами и обработчиками в одном файле - Логирование и бизнес-логика переплетены с signal receivers Решение: Разделен на модульную структуру documents/notifications/: - config.py - NotificationVerbs, MessageTemplates, ActionURLs, get_uploader_name(), get_bulk_threshold() - handlers.py - notify_all_employees(), notify_specific_users(), notify_department_employees(), notify_all_acknowledged(), notify_document_comment(), notify_comment_reply() - signals.py - Django signal receivers (@receiver) для Document, DocumentAcknowledgement, DocumentComment - __init__.py - публичный API модуля Изменения: - Создан: documents/notifications/ модуль с 4 файлами - Переименован: notification_signals.py → notification_signals.py.old - Изменен: apps.py - обновлен импорт на documents.notifications Логика уведомлений: 1. Новый документ → все сотрудники/отделы/конкретные получатели 2. Все ознакомились → уведомление загрузившему 3. Комментарий → автор документа 4. Ответ на комментарий → автор родительского комментария Преимущества: - ✅ Единообразие с requests_app, procurement, feed, communications - ✅ Четкое разделение ответственности (SRP) - ✅ Централизованное логирование с контекстом - ✅ Поддержка bulk режима для массовых рассылок - ✅ Легче тестировать компоненты - ✅ Публичный API для переиспользования Верификация: - python manage.py check - успешно
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
"""
|
||
Django signals для автоматической генерации уведомлений в модуле Documents.
|
||
|
||
Обрабатывает события:
|
||
- post_save для Document - создание нового документа
|
||
- m2m_changed для Document.recipients - добавление конкретных получателей
|
||
- m2m_changed для Document.departments - добавление отделов
|
||
- post_save для DocumentAcknowledgement - проверка полного ознакомления
|
||
- post_save для DocumentComment - новый комментарий или ответ
|
||
"""
|
||
|
||
import logging
|
||
|
||
from django.contrib.auth import get_user_model
|
||
from django.db.models.signals import post_save, m2m_changed
|
||
from django.dispatch import receiver
|
||
|
||
from ..models import Document, DocumentAcknowledgement, DocumentComment
|
||
from .handlers import (
|
||
notify_all_employees,
|
||
notify_specific_users,
|
||
notify_department_employees,
|
||
notify_all_acknowledged,
|
||
notify_document_comment,
|
||
notify_comment_reply,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
Employee = get_user_model()
|
||
|
||
|
||
@receiver(post_save, sender=Document)
|
||
def create_document_notification(sender, instance, created, **kwargs):
|
||
"""
|
||
Создает уведомления при создании нового документа.
|
||
|
||
Уведомления отправляются:
|
||
- Всем активным сотрудникам (если sent_to_all=True)
|
||
- Сотрудникам выбранных отделов (через m2m_changed signal)
|
||
- Выбранным получателям (через m2m_changed signal)
|
||
"""
|
||
logger.info(
|
||
f"[signals] post_save Document id={instance.pk} "
|
||
f"created={created} sent_to_all={instance.sent_to_all}"
|
||
)
|
||
|
||
if not created:
|
||
return
|
||
|
||
document = instance
|
||
|
||
# Если документ отправляется всем - создаем уведомления напрямую
|
||
# channels.py автоматически отправит через Celery
|
||
if document.sent_to_all:
|
||
logger.info(
|
||
f"[signals] Creating notifications for doc={document.pk} (sent_to_all=True)"
|
||
)
|
||
notify_all_employees(document)
|
||
else:
|
||
logger.info(
|
||
f"[signals] Skipping notifications (sent_to_all=False), "
|
||
f"waiting for m2m_changed"
|
||
)
|
||
|
||
|
||
@receiver(m2m_changed, sender=Document.recipients.through)
|
||
def notify_specific_recipients(sender, instance, action, pk_set, **kwargs):
|
||
"""
|
||
Уведомляет конкретных получателей при добавлении в список.
|
||
Использует быструю обработку для большого количества получателей.
|
||
"""
|
||
logger.info(
|
||
f"[signals] m2m_changed Document id={instance.pk} "
|
||
f"action={action} pk_set={pk_set} sent_to_all={instance.sent_to_all}"
|
||
)
|
||
|
||
if action != 'post_add':
|
||
logger.info(
|
||
f"[signals] Skipping (action={action} != 'post_add')"
|
||
)
|
||
return
|
||
|
||
document = instance
|
||
|
||
# Не отправляем индивидуальные уведомления если документ для всех
|
||
if document.sent_to_all:
|
||
logger.info(
|
||
"[signals] Skipping (sent_to_all=True)"
|
||
)
|
||
return
|
||
|
||
# Отправляем уведомления через handlers
|
||
notify_specific_users(document, pk_set)
|
||
|
||
|
||
@receiver(m2m_changed, sender=Document.departments.through)
|
||
def notify_department_employees_signal(sender, instance, action, pk_set, **kwargs):
|
||
"""
|
||
Уведомляет всех сотрудников выбранных отделов при добавлении отделов.
|
||
Уведомления получат ВСЕ текущие и будущие сотрудники этих отделов.
|
||
"""
|
||
logger.info(
|
||
f"[signals] m2m_changed Document.departments "
|
||
f"id={instance.pk} action={action} pk_set={pk_set}"
|
||
)
|
||
|
||
if action != 'post_add':
|
||
logger.info(
|
||
f"[signals] Skipping (action={action} != 'post_add')"
|
||
)
|
||
return
|
||
|
||
document = instance
|
||
|
||
# Не отправляем если документ для всех
|
||
if document.sent_to_all:
|
||
logger.info(
|
||
"[signals] Skipping (sent_to_all=True)"
|
||
)
|
||
return
|
||
|
||
# Отправляем уведомления через handlers
|
||
notify_department_employees(document, pk_set)
|
||
|
||
|
||
@receiver(post_save, sender=DocumentAcknowledgement)
|
||
def check_all_acknowledged(sender, instance, created, **kwargs):
|
||
"""
|
||
Проверяет, все ли получатели ознакомились с документом.
|
||
Если да - уведомляет загрузившего.
|
||
"""
|
||
if not created:
|
||
return
|
||
|
||
acknowledgement = instance
|
||
document = acknowledgement.document
|
||
|
||
# Определяем общее количество получателей
|
||
if document.sent_to_all:
|
||
total_recipients = Employee.objects.filter(is_active=True).count()
|
||
else:
|
||
# Считаем получателей из recipients + departments
|
||
recipients_count = document.recipients.count()
|
||
|
||
# Добавляем сотрудников из отделов
|
||
from employees.models import Department
|
||
department_employees = set()
|
||
for department in document.departments.all():
|
||
department_employees.update(department.active_employees)
|
||
|
||
# Исключаем дубликаты с recipients
|
||
total_recipients = recipients_count + len(department_employees)
|
||
|
||
# Проверяем количество ознакомившихся
|
||
acknowledged_count = document.acknowledgements.count()
|
||
|
||
# Если все ознакомились - уведомляем загрузившего
|
||
if (acknowledged_count >= total_recipients and
|
||
total_recipients > 0 and
|
||
document.uploaded_by):
|
||
|
||
notify_all_acknowledged(document, total_recipients, acknowledged_count)
|
||
|
||
|
||
@receiver(post_save, sender=DocumentComment)
|
||
def notify_on_new_comment(sender, instance, created, **kwargs):
|
||
"""
|
||
Отправка уведомлений при создании нового комментария.
|
||
|
||
- Если это ответ на комментарий (parent != None): уведомить автора родительского комментария
|
||
- Если это обычный комментарий: уведомить автора документа (если он не автор комментария)
|
||
"""
|
||
if not created:
|
||
return
|
||
|
||
comment = instance
|
||
document = comment.document
|
||
author = comment.author
|
||
|
||
logger.info(
|
||
f"[signals] New comment on document={document.pk} "
|
||
f"by user={author.pk} parent={comment.parent_id}"
|
||
)
|
||
|
||
# Если это ответ на комментарий
|
||
if comment.parent:
|
||
notify_comment_reply(comment.parent, comment)
|
||
else:
|
||
# Обычный комментарий - уведомляем автора документа
|
||
notify_document_comment(document, comment)
|