468 lines
16 KiB
Python
468 lines
16 KiB
Python
"""
|
||
Бизнес-логика отправки уведомлений для модуля 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 django.db.models import Q
|
||
|
||
from notifications.signals import notify
|
||
|
||
from .config import (
|
||
ActionURLs,
|
||
MessageTemplates,
|
||
NotificationVerbs,
|
||
get_bulk_threshold,
|
||
get_uploader_name,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
Employee = get_user_model()
|
||
|
||
|
||
def _regulation_notification_context(document):
|
||
"""Возвращает отделы и область действия регламента для уведомления."""
|
||
if not document.is_regulation:
|
||
return {}
|
||
|
||
if document.acknowledgement_required and not document.acknowledgement_for_all:
|
||
departments = document.acknowledgement_departments.all()
|
||
elif not document.sent_to_all:
|
||
departments = document.departments.all()
|
||
else:
|
||
departments = document.departments.none()
|
||
|
||
department_rows = list(departments.order_by("name", "id").values("id", "name"))
|
||
if department_rows:
|
||
scope = "department"
|
||
elif document.sent_to_all:
|
||
scope = "company"
|
||
else:
|
||
scope = "personal"
|
||
|
||
return {
|
||
"is_regulation": True,
|
||
"regulation_scope": scope,
|
||
"regulation_department_ids": [row["id"] for row in department_rows],
|
||
"regulation_department_names": [row["name"] for row in department_rows],
|
||
}
|
||
|
||
|
||
def _document_ready_payload(
|
||
document,
|
||
uploader_name,
|
||
recipient=None,
|
||
regulation_context=None,
|
||
):
|
||
"""Собирает единый текст и данные уведомления о новом документе."""
|
||
if recipient is None:
|
||
acknowledgement_required = bool(
|
||
document.acknowledgement_required and document.acknowledgement_for_all
|
||
)
|
||
else:
|
||
from documents.audience import user_requires_document_acknowledgement
|
||
|
||
acknowledgement_required = user_requires_document_acknowledgement(
|
||
document, recipient
|
||
)
|
||
return {
|
||
"description": MessageTemplates.document_ready(
|
||
uploader_name,
|
||
document.title,
|
||
acknowledgement_required=acknowledgement_required,
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
"data": {
|
||
"title": MessageTemplates.document_ready_title(
|
||
acknowledgement_required=acknowledgement_required,
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
"document_id": document.id,
|
||
"uploaded_by_id": (
|
||
document.uploaded_by.id if document.uploaded_by else None
|
||
),
|
||
"sent_to_all": document.sent_to_all,
|
||
"acknowledgement_required": acknowledgement_required,
|
||
"is_regulation": document.is_regulation,
|
||
**(
|
||
regulation_context
|
||
if regulation_context is not None
|
||
else _regulation_notification_context(document)
|
||
),
|
||
},
|
||
}
|
||
|
||
|
||
def _send_document_ready_notification(
|
||
document,
|
||
recipient,
|
||
uploader_name,
|
||
regulation_context=None,
|
||
):
|
||
payload = _document_ready_payload(
|
||
document,
|
||
uploader_name,
|
||
recipient,
|
||
regulation_context,
|
||
)
|
||
notify.send(
|
||
sender=document.uploaded_by,
|
||
recipient=recipient,
|
||
verb=NotificationVerbs.ready(document.is_regulation),
|
||
action_object=document,
|
||
description=payload["description"],
|
||
action_url=ActionURLs.document_detail(
|
||
document.id,
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
data=payload["data"],
|
||
)
|
||
|
||
|
||
def notify_document_ready(document, recipient):
|
||
"""
|
||
Отправляет уведомление конкретному получателю о новом документе.
|
||
|
||
Args:
|
||
document: Объект Document
|
||
recipient: Объект Employee
|
||
"""
|
||
logger.debug(
|
||
"Document notification creation started",
|
||
extra={
|
||
"event": "document.notification.creation_started",
|
||
"document_id": document.id,
|
||
"recipient_id": recipient.id,
|
||
},
|
||
)
|
||
|
||
uploader_name = get_uploader_name(document.uploaded_by)
|
||
_send_document_ready_notification(document, recipient, uploader_name)
|
||
|
||
|
||
def notify_all_employees(document):
|
||
"""
|
||
Отправляет уведомление о новом документе всем активным сотрудникам.
|
||
|
||
Args:
|
||
document: Объект Document
|
||
"""
|
||
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()
|
||
# Создаём уведомления
|
||
created_count = 0
|
||
uploader_name = get_uploader_name(document.uploaded_by)
|
||
regulation_context = _regulation_notification_context(document)
|
||
for employee in active_employees:
|
||
try:
|
||
_send_document_ready_notification(
|
||
document,
|
||
employee,
|
||
uploader_name,
|
||
regulation_context,
|
||
)
|
||
created_count += 1
|
||
except Exception as error:
|
||
logger.exception(
|
||
"Document notification creation failed",
|
||
extra={
|
||
"event": "document.notification.creation_failed",
|
||
"document_id": document.id,
|
||
"recipient_id": employee.id,
|
||
"error_type": type(error).__name__,
|
||
},
|
||
)
|
||
|
||
logger.info(
|
||
"Document notifications created",
|
||
extra={
|
||
"event": "document.notification.created",
|
||
"document_id": document.id,
|
||
"audience_type": "all_employees",
|
||
"recipient_count": count,
|
||
"created_count": created_count,
|
||
},
|
||
)
|
||
|
||
# Запускаем асинхронную отправку email/telegram
|
||
try:
|
||
import threading
|
||
|
||
from django.core.management import call_command
|
||
|
||
def send_pending():
|
||
try:
|
||
call_command("send_pending_notifications", "--batch-size=100")
|
||
except Exception as error:
|
||
logger.exception(
|
||
"Pending document notification delivery failed",
|
||
extra={
|
||
"event": "document.notification.delivery_failed",
|
||
"document_id": document.id,
|
||
"error_type": type(error).__name__,
|
||
},
|
||
)
|
||
|
||
# Запускаем в отдельном потоке
|
||
thread = threading.Thread(target=send_pending, daemon=True)
|
||
thread.start()
|
||
logger.debug(
|
||
"Pending document notification delivery started",
|
||
extra={
|
||
"event": "document.notification.delivery_started",
|
||
"document_id": document.id,
|
||
},
|
||
)
|
||
except Exception as error:
|
||
logger.exception(
|
||
"Pending document notification delivery could not start",
|
||
extra={
|
||
"event": "document.notification.delivery_start_failed",
|
||
"document_id": document.id,
|
||
"error_type": type(error).__name__,
|
||
},
|
||
)
|
||
|
||
|
||
def notify_specific_users(document, user_ids):
|
||
"""
|
||
Уведомляет конкретных получателей о документе.
|
||
|
||
Args:
|
||
document: Объект Document
|
||
user_ids: Набор ID пользователей
|
||
"""
|
||
recipient_count = len(user_ids or [])
|
||
# Определяем, это массовая рассылка или нет
|
||
is_bulk = recipient_count >= get_bulk_threshold()
|
||
|
||
if is_bulk:
|
||
logger.debug(
|
||
"Bulk document notification mode selected",
|
||
extra={
|
||
"event": "document.notification.bulk_mode_selected",
|
||
"document_id": document.id,
|
||
"recipient_count": recipient_count,
|
||
},
|
||
)
|
||
|
||
# Уведомляем получателей
|
||
created_count = 0
|
||
uploader_name = get_uploader_name(document.uploaded_by)
|
||
regulation_context = _regulation_notification_context(document)
|
||
for user_id in user_ids:
|
||
try:
|
||
user = Employee.objects.get(id=user_id)
|
||
_send_document_ready_notification(
|
||
document,
|
||
user,
|
||
uploader_name,
|
||
regulation_context,
|
||
)
|
||
created_count += 1
|
||
except Employee.DoesNotExist:
|
||
logger.warning(
|
||
"Document notification recipient not found",
|
||
extra={
|
||
"event": "document.notification.recipient_missing",
|
||
"document_id": document.id,
|
||
"recipient_id": user_id,
|
||
},
|
||
)
|
||
except Exception as error:
|
||
logger.exception(
|
||
"Document notification creation failed",
|
||
extra={
|
||
"event": "document.notification.creation_failed",
|
||
"document_id": document.id,
|
||
"recipient_id": user_id,
|
||
"error_type": type(error).__name__,
|
||
},
|
||
)
|
||
|
||
logger.info(
|
||
"Document notifications created",
|
||
extra={
|
||
"event": "document.notification.created",
|
||
"document_id": document.id,
|
||
"audience_type": "specific_users",
|
||
"recipient_count": recipient_count,
|
||
"created_count": created_count,
|
||
},
|
||
)
|
||
|
||
|
||
def notify_department_employees(document, department_ids):
|
||
"""
|
||
Уведомляет всех сотрудников выбранных отделов о документе.
|
||
|
||
Args:
|
||
document: Объект Document
|
||
department_ids: Набор ID отделов
|
||
"""
|
||
if not department_ids:
|
||
logger.warning(
|
||
"Document notifications skipped",
|
||
extra={
|
||
"event": "document.notification.skipped",
|
||
"document_id": document.id,
|
||
"reason": "no_departments",
|
||
},
|
||
)
|
||
return
|
||
|
||
# Роль отдела эквивалентна физическому членству в отделе.
|
||
all_employees = set(
|
||
Employee.objects.filter(is_active=True)
|
||
.filter(
|
||
Q(
|
||
departments_links__department_id__in=department_ids,
|
||
departments_links__is_active=True,
|
||
)
|
||
| Q(
|
||
role_assignments__role__department_id__in=department_ids,
|
||
role_assignments__is_active=True,
|
||
)
|
||
)
|
||
.distinct()
|
||
)
|
||
|
||
# Исключаем загрузившего
|
||
if document.uploaded_by and document.uploaded_by in all_employees:
|
||
all_employees.remove(document.uploaded_by)
|
||
|
||
recipient_count = len(all_employees)
|
||
# Определяем режим рассылки
|
||
is_bulk = recipient_count >= get_bulk_threshold()
|
||
|
||
if is_bulk:
|
||
logger.debug(
|
||
"Bulk document notification mode selected",
|
||
extra={
|
||
"event": "document.notification.bulk_mode_selected",
|
||
"document_id": document.id,
|
||
"recipient_count": recipient_count,
|
||
},
|
||
)
|
||
|
||
# Создаём уведомления
|
||
created_count = 0
|
||
uploader_name = get_uploader_name(document.uploaded_by)
|
||
regulation_context = _regulation_notification_context(document)
|
||
for employee in all_employees:
|
||
try:
|
||
_send_document_ready_notification(
|
||
document,
|
||
employee,
|
||
uploader_name,
|
||
regulation_context,
|
||
)
|
||
created_count += 1
|
||
except Exception as error:
|
||
logger.exception(
|
||
"Document notification creation failed",
|
||
extra={
|
||
"event": "document.notification.creation_failed",
|
||
"document_id": document.id,
|
||
"recipient_id": employee.id,
|
||
"error_type": type(error).__name__,
|
||
},
|
||
)
|
||
|
||
logger.info(
|
||
"Document notifications created",
|
||
extra={
|
||
"event": "document.notification.created",
|
||
"document_id": document.id,
|
||
"audience_type": "departments",
|
||
"department_count": len(department_ids),
|
||
"recipient_count": recipient_count,
|
||
"created_count": created_count,
|
||
},
|
||
)
|
||
|
||
|
||
def notify_document_audience(document):
|
||
"""Уведомляет аудиторию доступа после полной настройки документа."""
|
||
if document.sent_to_all:
|
||
notify_all_employees(document)
|
||
return
|
||
|
||
from documents.audience import document_access_audience
|
||
|
||
recipient_ids = set(document_access_audience(document).values_list("pk", flat=True))
|
||
if document.uploaded_by_id:
|
||
recipient_ids.discard(document.uploaded_by_id)
|
||
notify_specific_users(document, recipient_ids)
|
||
|
||
|
||
def notify_all_acknowledged(document, total_recipients, acknowledged_count):
|
||
"""
|
||
Уведомляет загрузившего о том, что все ознакомились с документом.
|
||
|
||
Args:
|
||
document: Объект Document
|
||
total_recipients: Общее количество получателей
|
||
acknowledged_count: Количество ознакомившихся
|
||
"""
|
||
if not document.uploaded_by:
|
||
logger.info(
|
||
"Document acknowledgement completion notification skipped",
|
||
extra={
|
||
"event": "document.acknowledgement_notification.skipped",
|
||
"document_id": document.pk,
|
||
"reason": "uploader_missing",
|
||
},
|
||
)
|
||
return
|
||
|
||
logger.info(
|
||
"Document acknowledgement completion notification started",
|
||
extra={
|
||
"event": "document.acknowledgement_notification.started",
|
||
"document_id": document.pk,
|
||
"uploader_id": document.uploaded_by_id,
|
||
"acknowledged_count": acknowledged_count,
|
||
"recipient_count": total_recipients,
|
||
},
|
||
)
|
||
|
||
notify.send(
|
||
sender=None,
|
||
recipient=document.uploaded_by,
|
||
verb=NotificationVerbs.signed_all(document.is_regulation),
|
||
action_object=document,
|
||
description=MessageTemplates.all_acknowledged(
|
||
document.title,
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
action_url=ActionURLs.document_detail(
|
||
document.id,
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
data={
|
||
"title": MessageTemplates.all_acknowledged_title(
|
||
is_regulation=document.is_regulation,
|
||
),
|
||
"document_id": document.id,
|
||
"total_recipients": total_recipients,
|
||
"acknowledged_count": acknowledged_count,
|
||
"is_regulation": document.is_regulation,
|
||
**_regulation_notification_context(document),
|
||
},
|
||
)
|