2026-09-07 19:43:48 +03:00

468 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Бизнес-логика отправки уведомлений для модуля 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),
},
)