refactor(documents): реструктуризация notifications в модульную архитектуру
Проблема: - Монолитный 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 - успешно
This commit is contained in:
parent
ca80321034
commit
cc24a073f4
@ -6,6 +6,6 @@ class DocumentsConfig(AppConfig):
|
||||
name = "documents"
|
||||
|
||||
def ready(self):
|
||||
# импортируем сигналы, чтобы они подхватились
|
||||
import documents.notification_signals # noqa
|
||||
# Модуль уведомлений (signals регистрируются автоматически)
|
||||
import documents.notifications # noqa
|
||||
import documents.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
35
backend/documents/notifications/__init__.py
Normal file
35
backend/documents/notifications/__init__.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
Модуль уведомлений для Documents приложения.
|
||||
|
||||
Публичный API:
|
||||
- NotificationVerbs - константы глаголов уведомлений
|
||||
- MessageTemplates - шаблоны сообщений
|
||||
- ActionURLs - генераторы URL-адресов
|
||||
- notify_document_ready - функция для ручного уведомления о документе
|
||||
|
||||
Использование:
|
||||
from documents.notifications import notify_document_ready
|
||||
|
||||
# В коде:
|
||||
notify_document_ready(document, user)
|
||||
"""
|
||||
|
||||
from .config import (
|
||||
NotificationVerbs,
|
||||
MessageTemplates,
|
||||
ActionURLs,
|
||||
get_uploader_name,
|
||||
)
|
||||
from .handlers import notify_document_ready
|
||||
|
||||
# Импортируем handlers и signals для регистрации
|
||||
from . import handlers # noqa: F401
|
||||
from . import signals # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
'NotificationVerbs',
|
||||
'MessageTemplates',
|
||||
'ActionURLs',
|
||||
'notify_document_ready',
|
||||
'get_uploader_name',
|
||||
]
|
||||
114
backend/documents/notifications/config.py
Normal file
114
backend/documents/notifications/config.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""
|
||||
Конфигурация уведомлений для модуля Documents.
|
||||
|
||||
Содержит:
|
||||
- NotificationVerbs - глаголы для уведомлений
|
||||
- MessageTemplates - шаблоны сообщений
|
||||
- ActionURLs - URL-адреса действий
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class NotificationVerbs:
|
||||
"""Глаголы (verbs) для уведомлений documents."""
|
||||
|
||||
DOCUMENT_READY = 'document_ready'
|
||||
DOCUMENT_SIGNED_ALL = 'document_signed_all'
|
||||
DOCUMENT_COMMENT = 'document_comment'
|
||||
DOCUMENT_COMMENT_REPLY = 'document_comment_reply'
|
||||
|
||||
|
||||
class MessageTemplates:
|
||||
"""Шаблоны сообщений для уведомлений documents."""
|
||||
|
||||
@staticmethod
|
||||
def document_ready(uploader_name: str, document_title: str) -> str:
|
||||
"""Шаблон для нового документа на ознакомление."""
|
||||
return (
|
||||
f'{uploader_name} загрузил документ "{document_title}". '
|
||||
f'Требуется ознакомление.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def document_ready_title() -> str:
|
||||
"""Заголовок для нового документа."""
|
||||
return 'Новый документ на ознакомление'
|
||||
|
||||
@staticmethod
|
||||
def all_acknowledged(document_title: str) -> str:
|
||||
"""Шаблон для завершения ознакомления всеми."""
|
||||
return f'Все сотрудники ознакомились с документом "{document_title}"'
|
||||
|
||||
@staticmethod
|
||||
def all_acknowledged_title() -> str:
|
||||
"""Заголовок для завершения ознакомления."""
|
||||
return 'Все ознакомились с документом'
|
||||
|
||||
@staticmethod
|
||||
def comment(author_name: str, document_title: str) -> str:
|
||||
"""Шаблон для нового комментария к документу."""
|
||||
return (
|
||||
f'{author_name} оставил комментарий '
|
||||
f'к вашему документу "{document_title}"'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def comment_title() -> str:
|
||||
"""Заголовок для комментария к документу."""
|
||||
return 'Новый комментарий к документу'
|
||||
|
||||
@staticmethod
|
||||
def comment_reply(author_name: str, document_title: str) -> str:
|
||||
"""Шаблон для ответа на комментарий."""
|
||||
return (
|
||||
f'{author_name} ответил на ваш '
|
||||
f'комментарий к документу "{document_title}"'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def comment_reply_title() -> str:
|
||||
"""Заголовок для ответа на комментарий."""
|
||||
return 'Ответ на ваш комментарий'
|
||||
|
||||
|
||||
class ActionURLs:
|
||||
"""URL-адреса для действий с документами."""
|
||||
|
||||
DOCUMENTS = '/documents'
|
||||
|
||||
@staticmethod
|
||||
def document_detail(document_id: int) -> str:
|
||||
"""
|
||||
Возвращает URL-адрес документа.
|
||||
|
||||
TODO: реализовать прямые ссылки на конкретные документы
|
||||
"""
|
||||
return f'/documents?id={document_id}'
|
||||
|
||||
|
||||
# ===== Константы =====
|
||||
|
||||
def get_bulk_threshold() -> int:
|
||||
"""
|
||||
Возвращает порог для определения массовой рассылки.
|
||||
|
||||
Returns:
|
||||
int: Количество получателей, начиная с которого используется bulk режим
|
||||
"""
|
||||
return getattr(settings, 'NOTIFICATION_BULK_THRESHOLD', 10)
|
||||
|
||||
|
||||
def get_uploader_name(uploaded_by) -> str:
|
||||
"""
|
||||
Возвращает имя загрузившего документ пользователя.
|
||||
|
||||
Args:
|
||||
uploaded_by: Объект Employee или None
|
||||
|
||||
Returns:
|
||||
str: Полное имя или 'Администратор'
|
||||
"""
|
||||
if uploaded_by:
|
||||
return uploaded_by.get_full_name() or uploaded_by.username
|
||||
return 'Администратор'
|
||||
404
backend/documents/notifications/handlers.py
Normal file
404
backend/documents/notifications/handlers.py
Normal file
@ -0,0 +1,404 @@
|
||||
"""
|
||||
Бизнес-логика отправки уведомлений для модуля Documents.
|
||||
|
||||
Функции:
|
||||
- notify_document_ready - уведомление о новом документе
|
||||
- notify_all_employees - уведомление всем сотрудникам
|
||||
- notify_specific_users - уведомление конкретным пользователям
|
||||
- notify_department_employees - уведомление сотрудникам отделов
|
||||
- notify_all_acknowledged - уведомление о завершении ознакомления
|
||||
- notify_document_comment - уведомление о комментарии к документу
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from notifications.signals import notify
|
||||
|
||||
from .config import (
|
||||
NotificationVerbs,
|
||||
MessageTemplates,
|
||||
ActionURLs,
|
||||
get_bulk_threshold,
|
||||
get_uploader_name,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
Employee = get_user_model()
|
||||
|
||||
|
||||
def notify_document_ready(document, recipient):
|
||||
"""
|
||||
Отправляет уведомление конкретному получателю о новом документе.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
recipient: Объект Employee
|
||||
"""
|
||||
logger.info(
|
||||
f"[handlers] create_document_ready_notification "
|
||||
f"doc={document.id} recipient={recipient.id}"
|
||||
)
|
||||
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
|
||||
notify.send(
|
||||
sender=document.uploaded_by,
|
||||
recipient=recipient,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(uploader_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.document_ready_title(),
|
||||
'document_id': document.id,
|
||||
'uploaded_by_id': document.uploaded_by.id if document.uploaded_by else None,
|
||||
'sent_to_all': document.sent_to_all,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def notify_all_employees(document):
|
||||
"""
|
||||
Отправляет уведомление о новом документе всем активным сотрудникам.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
"""
|
||||
logger.info(
|
||||
f"[handlers] notify_all_employees doc={document.id}"
|
||||
)
|
||||
|
||||
active_employees = Employee.objects.filter(is_active=True)
|
||||
|
||||
# Исключаем загрузившего документ
|
||||
if document.uploaded_by:
|
||||
active_employees = active_employees.exclude(id=document.uploaded_by.id)
|
||||
|
||||
count = active_employees.count()
|
||||
logger.info(
|
||||
f"[handlers] Creating {count} notifications "
|
||||
f"(web immediately, email/telegram deferred)"
|
||||
)
|
||||
|
||||
# Создаём уведомления
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
|
||||
for employee in active_employees:
|
||||
try:
|
||||
notify.send(
|
||||
sender=document.uploaded_by,
|
||||
recipient=employee,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(uploader_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.document_ready_title(),
|
||||
'document_id': document.id,
|
||||
'uploaded_by_id': (
|
||||
document.uploaded_by.id if document.uploaded_by else None
|
||||
),
|
||||
'sent_to_all': document.sent_to_all,
|
||||
},
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[handlers] Error creating notification for user {employee.id}: {e}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[handlers] Created {created_count}/{count} notifications "
|
||||
f"for doc={document.id}. Web sent immediately, email/telegram pending."
|
||||
)
|
||||
|
||||
# Запускаем асинхронную отправку email/telegram
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
import threading
|
||||
|
||||
def send_pending():
|
||||
try:
|
||||
call_command('send_pending_notifications', '--batch-size=100')
|
||||
except Exception as e:
|
||||
logger.error(f"[handlers] Error in background send: {e}")
|
||||
|
||||
# Запускаем в отдельном потоке
|
||||
thread = threading.Thread(target=send_pending, daemon=True)
|
||||
thread.start()
|
||||
logger.info("[handlers] Started background email/telegram send")
|
||||
except Exception as e:
|
||||
logger.error(f"[handlers] Error starting background send: {e}")
|
||||
|
||||
|
||||
def notify_specific_users(document, user_ids):
|
||||
"""
|
||||
Уведомляет конкретных получателей о документе.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
user_ids: Набор ID пользователей
|
||||
"""
|
||||
recipient_count = len(user_ids or [])
|
||||
logger.info(
|
||||
f"[handlers] Processing {recipient_count} specific recipients"
|
||||
)
|
||||
|
||||
# Определяем, это массовая рассылка или нет
|
||||
is_bulk = recipient_count >= get_bulk_threshold()
|
||||
|
||||
if is_bulk:
|
||||
logger.info(
|
||||
f"[handlers] Bulk mode activated "
|
||||
f"({recipient_count} >= {get_bulk_threshold()})"
|
||||
)
|
||||
|
||||
# Уведомляем получателей
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
user = Employee.objects.get(id=user_id)
|
||||
logger.debug(
|
||||
f"[handlers] Creating notification for user={user_id}"
|
||||
)
|
||||
notify.send(
|
||||
sender=document.uploaded_by,
|
||||
recipient=user,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(uploader_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.document_ready_title(),
|
||||
'document_id': document.id,
|
||||
'uploaded_by_id': (
|
||||
document.uploaded_by.id if document.uploaded_by else None
|
||||
),
|
||||
'sent_to_all': document.sent_to_all,
|
||||
},
|
||||
)
|
||||
created_count += 1
|
||||
except Employee.DoesNotExist:
|
||||
logger.warning(f"[handlers] User {user_id} not found, skipping")
|
||||
except Exception as e:
|
||||
logger.error(f"[handlers] Error creating notification for user {user_id}: {e}")
|
||||
|
||||
logger.info(f"[handlers] Created {created_count} notifications")
|
||||
|
||||
|
||||
def notify_department_employees(document, department_ids):
|
||||
"""
|
||||
Уведомляет всех сотрудников выбранных отделов о документе.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
department_ids: Набор ID отделов
|
||||
"""
|
||||
logger.info(
|
||||
f"[handlers] Processing {len(department_ids)} departments"
|
||||
)
|
||||
|
||||
if not department_ids:
|
||||
logger.warning("[handlers] No departments provided")
|
||||
return
|
||||
|
||||
# Получаем всех активных сотрудников выбранных отделов
|
||||
from employees.models import Department
|
||||
|
||||
all_employees = set()
|
||||
for dept_id in department_ids:
|
||||
try:
|
||||
department = Department.objects.get(id=dept_id)
|
||||
dept_employees = department.active_employees
|
||||
logger.info(
|
||||
f"[handlers] Department '{department.name}' "
|
||||
f"has {len(dept_employees)} active employees: "
|
||||
f"{[e.id for e in dept_employees]}"
|
||||
)
|
||||
all_employees.update(dept_employees)
|
||||
logger.info(
|
||||
f"[handlers] After update, all_employees has: "
|
||||
f"{[e.id for e in all_employees]}"
|
||||
)
|
||||
except Department.DoesNotExist:
|
||||
logger.warning(
|
||||
f"[handlers] Department {dept_id} not found"
|
||||
)
|
||||
continue
|
||||
|
||||
# Исключаем загрузившего
|
||||
if document.uploaded_by and document.uploaded_by in all_employees:
|
||||
all_employees.remove(document.uploaded_by)
|
||||
|
||||
recipient_count = len(all_employees)
|
||||
logger.info(
|
||||
f"[handlers] Processing {recipient_count} employees "
|
||||
f"from {len(department_ids)} departments"
|
||||
)
|
||||
|
||||
# Определяем режим рассылки
|
||||
is_bulk = recipient_count >= get_bulk_threshold()
|
||||
|
||||
if is_bulk:
|
||||
logger.info(
|
||||
f"[handlers] Bulk mode activated "
|
||||
f"({recipient_count} >= {get_bulk_threshold()})"
|
||||
)
|
||||
|
||||
# Создаём уведомления
|
||||
created_count = 0
|
||||
uploader_name = get_uploader_name(document.uploaded_by)
|
||||
|
||||
for employee in all_employees:
|
||||
try:
|
||||
notify.send(
|
||||
sender=document.uploaded_by,
|
||||
recipient=employee,
|
||||
verb=NotificationVerbs.DOCUMENT_READY,
|
||||
action_object=document,
|
||||
description=MessageTemplates.document_ready(uploader_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.document_ready_title(),
|
||||
'document_id': document.id,
|
||||
'uploaded_by_id': (
|
||||
document.uploaded_by.id if document.uploaded_by else None
|
||||
),
|
||||
'sent_to_all': document.sent_to_all,
|
||||
},
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[handlers] Error creating notification for employee {employee.id}: {e}")
|
||||
|
||||
logger.info(f"[handlers] Created {created_count} notifications")
|
||||
|
||||
|
||||
def notify_all_acknowledged(document, total_recipients, acknowledged_count):
|
||||
"""
|
||||
Уведомляет загрузившего о том, что все ознакомились с документом.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
total_recipients: Общее количество получателей
|
||||
acknowledged_count: Количество ознакомившихся
|
||||
"""
|
||||
if not document.uploaded_by:
|
||||
logger.info(
|
||||
"[handlers] Document has no uploader, skipping acknowledgement notification"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[handlers] Notifying uploader about full acknowledgement "
|
||||
f"({acknowledged_count}/{total_recipients})"
|
||||
)
|
||||
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=document.uploaded_by,
|
||||
verb=NotificationVerbs.DOCUMENT_SIGNED_ALL,
|
||||
action_object=document,
|
||||
description=MessageTemplates.all_acknowledged(document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.all_acknowledged_title(),
|
||||
'document_id': document.id,
|
||||
'total_recipients': total_recipients,
|
||||
'acknowledged_count': acknowledged_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def notify_document_comment(document, comment):
|
||||
"""
|
||||
Уведомляет автора документа о новом комментарии.
|
||||
|
||||
Args:
|
||||
document: Объект Document
|
||||
comment: Объект DocumentComment
|
||||
"""
|
||||
author = comment.author
|
||||
|
||||
if not document.uploaded_by:
|
||||
logger.info(
|
||||
"[handlers] Document has no author, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
doc_author = document.uploaded_by
|
||||
|
||||
# Не уведомляем самого себя
|
||||
if doc_author.id == author.id:
|
||||
logger.info(
|
||||
"[handlers] Skipping self-comment notification"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[handlers] Notifying document author={doc_author.pk} "
|
||||
f"about new comment"
|
||||
)
|
||||
|
||||
author_name = author.get_full_name() or author.username
|
||||
|
||||
notify.send(
|
||||
sender=author,
|
||||
recipient=doc_author,
|
||||
verb=NotificationVerbs.DOCUMENT_COMMENT,
|
||||
action_object=comment,
|
||||
description=MessageTemplates.comment(author_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.comment_title(),
|
||||
'document_id': document.id,
|
||||
'comment_id': comment.id,
|
||||
'author_id': author.id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def notify_comment_reply(parent_comment, reply_comment):
|
||||
"""
|
||||
Уведомляет автора комментария об ответе на него.
|
||||
|
||||
Args:
|
||||
parent_comment: Объект DocumentComment (родительский)
|
||||
reply_comment: Объект DocumentComment (ответ)
|
||||
"""
|
||||
author = reply_comment.author
|
||||
parent_author = parent_comment.author
|
||||
document = reply_comment.document
|
||||
|
||||
# Не уведомляем самого себя
|
||||
if parent_author.id == author.id:
|
||||
logger.info(
|
||||
"[handlers] Skipping self-reply notification"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[handlers] Notifying parent author={parent_author.pk} about reply"
|
||||
)
|
||||
|
||||
author_name = author.get_full_name() or author.username
|
||||
|
||||
notify.send(
|
||||
sender=author,
|
||||
recipient=parent_author,
|
||||
verb=NotificationVerbs.DOCUMENT_COMMENT_REPLY,
|
||||
action_object=reply_comment,
|
||||
description=MessageTemplates.comment_reply(author_name, document.title),
|
||||
action_url=ActionURLs.DOCUMENTS,
|
||||
data={
|
||||
'title': MessageTemplates.comment_reply_title(),
|
||||
'document_id': document.id,
|
||||
'comment_id': reply_comment.id,
|
||||
'parent_comment_id': parent_comment.id,
|
||||
'author_id': author.id,
|
||||
},
|
||||
)
|
||||
190
backend/documents/notifications/signals.py
Normal file
190
backend/documents/notifications/signals.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user