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

416 lines
15 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.

"""
Обработчики (handlers) для отправки уведомлений о заявлениях.
Содержит бизнес-логику формирования и отправки уведомлений
для различных событий в модуле Requests.
"""
import logging
from django.contrib.auth import get_user_model
from notifications.signals import notify
from .config import ActionURLs, MessageTemplates, NotificationVerbs
Employee = get_user_model()
logger = logging.getLogger(__name__)
def notify_new_request(request_obj):
"""
Отправляет уведомление о новом заявлении:
- Всем основным получателям (recipients) - адресованное им
- Всем в копии (cc_users) - с пометкой "в копии"
- Согласующему (approver)
- Руководителям отделов
При sent_to_all_department=True отправляет всем сотрудникам отделов
Args:
request_obj: Объект Request
"""
recipients_set = set()
# 1. Основные получатели
recipients_count = request_obj.recipients.count()
for recipient in request_obj.recipients.filter(is_active=True):
recipients_set.add(recipient)
# 2. Копия (CC)
cc_count = request_obj.cc_users.count()
for cc_user in request_obj.cc_users.filter(is_active=True):
recipients_set.add(cc_user)
# 3. Если sent_to_all_department - все сотрудники отделов
if request_obj.sent_to_all_department:
dept_employees = (
Employee.objects.filter(
departments_links__department__in=(request_obj.departments.all()),
departments_links__is_active=True,
is_active=True,
)
.exclude(id=request_obj.employee.id)
.distinct()
)
recipients_set.update(dept_employees)
# 4. Согласующий
if request_obj.approver and request_obj.approver.id != request_obj.employee.id:
recipients_set.add(request_obj.approver)
# 5. Руководители отделов
for department in request_obj.departments.all():
if department.head and department.head.id != request_obj.employee.id:
recipients_set.add(department.head)
# Также проверяем старое поле department для обратной совместимости
if request_obj.department and request_obj.department.head:
if request_obj.department.head.id != request_obj.employee.id:
recipients_set.add(request_obj.department.head)
if len(recipients_set) == 0:
logger.warning(
"New request notification has no recipients",
extra={
"event": "request.notification.skipped",
"business_request_id": request_obj.id,
"reason": "no_recipients",
"configured_recipient_count": recipients_count,
"configured_cc_count": cc_count,
},
)
return
# Подготовка общих данных
author_name = request_obj.employee.get_full_name() or request_obj.employee.username
request_type = request_obj.get_type_display()
comment_preview = (
request_obj.comment[:150] if request_obj.comment else "Без комментария"
)
# Отправка уведомлений каждому получателю
for recipient in recipients_set:
_send_new_request_notification(
request_obj=request_obj,
recipient=recipient,
author_name=author_name,
request_type=request_type,
comment_preview=comment_preview,
)
logger.info(
"New request notifications sent",
extra={
"event": "request.notification.sent",
"business_request_id": request_obj.id,
"notification_type": "new_request",
"recipient_count": len(recipients_set),
"configured_recipient_count": recipients_count,
"configured_cc_count": cc_count,
},
)
def _send_new_request_notification(
request_obj, recipient, author_name, request_type, comment_preview
):
"""
Отправляет уведомление о новом заявлении конкретному получателю.
Определяет роль получателя и формирует соответствующее сообщение.
Args:
request_obj: Объект Request
recipient: Получатель (Employee)
author_name: Имя автора заявления
request_type: Тип заявления (отображаемое название)
comment_preview: Превью комментария
"""
# Определяем роль получателя
is_primary = request_obj.recipients.filter(id=recipient.id).exists()
is_cc = request_obj.cc_users.filter(id=recipient.id).exists()
is_approver = request_obj.approver_id == recipient.id
# Формируем заголовок и сообщение с учетом роли
if is_primary:
title, message = MessageTemplates.new_request_primary(
author_name, request_type, comment_preview
)
elif is_cc:
title, message = MessageTemplates.new_request_cc(
author_name, request_type, comment_preview
)
elif is_approver:
title, message = MessageTemplates.new_request_approver(
author_name, request_type, comment_preview
)
else:
title, message = MessageTemplates.new_request_department(
author_name, request_type, comment_preview
)
notify.send(
sender=request_obj.employee,
recipient=recipient,
verb=NotificationVerbs.REQUEST_NEW,
action_object=request_obj,
description=message,
action_url=ActionURLs.REQUESTS_LIST,
data={
"title": title,
"request_id": request_obj.id,
"request_type": request_obj.type,
"employee_id": request_obj.employee.id,
"employee_name": author_name,
"is_primary_recipient": is_primary,
"is_cc": is_cc,
"is_approver": is_approver,
},
)
def notify_status_change(request_obj, old_status, new_status):
"""
Уведомляет о изменении статуса:
- Всех получателей (recipients)
- Всех в копии (cc_users)
- Сотрудников отделов (если sent_to_all_department)
ВАЖНО:
- Автор (employee) ВСЕГДА получает уведомление о решении (approve/reject)
- Approver (тот кто принял решение) НЕ получает уведомление
о своем же решении
Args:
request_obj: Объект Request
old_status: Старый статус
new_status: Новый статус
"""
recipients_to_notify = set()
# ID пользователей которых НЕ нужно уведомлять при approve/reject
exclude_ids = set()
if new_status in ("approved", "rejected"):
# ВАЖНО: Уведомляем автора заявки о решении - это единственный способ
# информировать его, так как alert() был убран из UI (коммит 935c12a)
# Не уведомляем только того кто принял решение (он сам нажал кнопку)
if request_obj.approver_id:
exclude_ids.add(request_obj.approver_id)
# 1. Автор - УВЕДОМЛЯЕМ всегда (даже при approved/rejected, это важно!)
recipients_to_notify.add(request_obj.employee)
# 2. Основные получатели (исключая тех кто в exclude_ids)
recipients_to_notify.update(
request_obj.recipients.filter(is_active=True).exclude(id__in=exclude_ids)
)
# 3. Копия (исключая тех кто в exclude_ids)
recipients_to_notify.update(
request_obj.cc_users.filter(is_active=True).exclude(id__in=exclude_ids)
)
# 4. Если sent_to_all_department - все сотрудники отделов (исключая
# exclude_ids)
if request_obj.sent_to_all_department:
# При approve/reject также исключаем approver
all_exclude_ids = {request_obj.employee.id}
all_exclude_ids.update(exclude_ids)
dept_employees = (
Employee.objects.filter(
departments_links__department__in=request_obj.departments.all(),
departments_links__is_active=True,
is_active=True,
)
.exclude(id__in=all_exclude_ids)
.distinct()
)
recipients_to_notify.update(dept_employees)
# Подготовка общих данных
request_type = request_obj.get_type_display()
employee_name = (
request_obj.employee.get_full_name() or request_obj.employee.username
)
approver_name = (
request_obj.approver.get_full_name() if request_obj.approver else "Руководитель"
)
# Отправка уведомлений каждому получателю
for recipient in recipients_to_notify:
_send_status_change_notification(
request_obj=request_obj,
recipient=recipient,
new_status=new_status,
old_status=old_status,
employee_name=employee_name,
request_type=request_type,
approver_name=approver_name,
)
logger.info(
"Request status notifications sent",
extra={
"event": "request.notification.sent",
"business_request_id": request_obj.id,
"notification_type": "status_change",
"old_status": old_status,
"new_status": new_status,
"recipient_count": len(recipients_to_notify),
},
)
def _send_status_change_notification(
request_obj,
recipient,
new_status,
old_status,
employee_name,
request_type,
approver_name,
):
"""
Отправляет уведомление об изменении статуса конкретному получателю.
Args:
request_obj: Объект Request
recipient: Получатель (Employee)
new_status: Новый статус
old_status: Старый статус
employee_name: Имя автора заявления
request_type: Тип заявления
approver_name: Имя согласующего
"""
# Определяем тип уведомления и формируем сообщение
if new_status == "approved":
verb = NotificationVerbs.REQUEST_APPROVED
title, message = MessageTemplates.status_approved(
employee_name, request_type, approver_name
)
elif new_status == "rejected":
verb = NotificationVerbs.REQUEST_REJECTED
title, message = MessageTemplates.status_rejected(
employee_name, request_type, approver_name
)
else:
verb = NotificationVerbs.REQUEST_STATUS_CHANGED
title, message = MessageTemplates.status_changed(
employee_name, request_type, old_status, new_status
)
notify.send(
sender=request_obj.approver if request_obj.approver else None,
recipient=recipient,
verb=verb,
action_object=request_obj,
description=message,
action_url=ActionURLs.REQUESTS_LIST,
data={
"title": title,
"request_id": request_obj.id,
"request_type": request_obj.type,
"employee_id": request_obj.employee.id,
"employee_name": employee_name,
"old_status": old_status,
"new_status": new_status,
"approver_id": (request_obj.approver.id if request_obj.approver else None),
"approver_name": approver_name,
},
)
def notify_comment(comment_obj):
"""
Отправляет уведомления о новом комментарии к заявлению.
Уведомляет:
- Автора заявления
- Всех получателей
- Всех в копии
- Согласующего
- Сотрудников отделов (если sent_to_all_department)
Args:
comment_obj: Объект RequestComment
"""
author = comment_obj.author
recipients_set = set()
request_obj = comment_obj.request
# Автор заявки
if request_obj.employee.id != author.id:
recipients_set.add(request_obj.employee)
# Получатели
recipients_set.update(
request_obj.recipients.filter(is_active=True).exclude(id=author.id)
)
# CC
recipients_set.update(
request_obj.cc_users.filter(is_active=True).exclude(id=author.id)
)
# Согласующий
if request_obj.approver and request_obj.approver.id != author.id:
recipients_set.add(request_obj.approver)
# Если sent_to_all_department - все сотрудники отделов
if request_obj.sent_to_all_department:
dept_employees = (
Employee.objects.filter(
departments_links__department__in=request_obj.departments.all(),
departments_links__is_active=True,
is_active=True,
)
.exclude(id__in=[author.id, request_obj.employee.id])
.distinct()
)
recipients_set.update(dept_employees)
# Подготовка данных
author_name = author.get_full_name() or author.username
request_type = request_obj.get_type_display()
employee_name = (
request_obj.employee.get_full_name() or request_obj.employee.username
)
# Отправка уведомлений
for recipient in recipients_set:
# Не отправляем уведомление тому, кто сам написал комментарий
if recipient.id == author.id:
continue
title, description = MessageTemplates.comment(
author_name, request_type, employee_name, comment_obj.text
)
notify.send(
sender=author,
recipient=recipient,
verb=NotificationVerbs.REQUEST_COMMENT,
action_object=request_obj,
description=description,
action_url=ActionURLs.REQUESTS_LIST,
data={
"title": title,
"request_id": request_obj.id,
"request_type": request_obj.type,
"comment_id": comment_obj.id,
"author_id": author.id,
},
)
logger.info(
"Request comment notifications sent",
extra={
"event": "request.notification.sent",
"business_request_id": request_obj.id,
"notification_type": "comment",
"comment_id": comment_obj.id,
"recipient_count": len(recipients_set),
},
)