Corp/backend/requests_app/notification_signals.py
Игорь Надеин c6002cff1b fix: исправлены ошибки в системе уведомлений
- Добавлен эндпоинт удаления комментариев к заявкам (DELETE /api/v1/requests/{pk}/comments/{comment_id}/)
- Исправлены references на переменную в сигналах комментариев (comment -> instance)
- Добавлена обработка статуса cancelled для заявок в notification_type_map
- Обновлены процессоры уведомлений в task_base.py (правильные поля моделей)
- Создан скрипт генерации тестовых пользователей (scripts/create_test_users.py)
2026-01-09 16:57:48 +03:00

579 lines
25 KiB
Python
Raw 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.

"""
Signals для автоматической генерации уведомлений в модуле Requests.
Обрабатывает события:
- Новое заявление (для ответственных/руководителей)
- Одобрение заявления
- Отклонение заявления
- Комментарий к заявлению
- Изменение статуса заявления
"""
import logging
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models.signals import m2m_changed, post_save, pre_save
from django.dispatch import receiver
from notifications.services import NotificationService
from .models import Request, RequestComment
Employee = get_user_model()
logger = logging.getLogger(__name__)
# Флаг для отслеживания новых заявлений, ожидающих уведомления
_pending_new_requests = set()
@receiver(post_save, sender=Request)
def create_request_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании или изменении заявления.
Обрабатывает:
1. Новое заявление - помечаем для отправки уведомлений после установки recipients
2. Изменение статуса - уведомление автору
"""
try:
request_obj = instance
# Логирование для отладки
print(
f"🔔 [SIGNAL] create_request_notifications: created={created}, status={request_obj.status}, id={request_obj.id}"
)
if created and request_obj.status != "draft":
# Помечаем заявление как ожидающее отправки уведомлений
# Уведомления будут отправлены после установки recipients через m2m_changed
print(
f"📌 [SIGNAL] Помечаем заявление #{request_obj.id} для отправки уведомлений после установки recipients"
)
_pending_new_requests.add(request_obj.id)
else:
print(
f"⏭️ [SIGNAL] Пропускаем: created={created}, status={request_obj.status}"
)
if not created:
# Проверяем изменение статуса через сохраненный атрибут _old_status
if hasattr(request_obj, "_old_status"):
old_status = request_obj._old_status
new_status = request_obj.status
if old_status != new_status:
from notifications.tasks import process_request_notifications_task
# Определяем action по статусу
action = 'approved' if new_status == 'approved' else \
'rejected' if new_status == 'rejected' else \
'cancelled' if new_status == 'cancelled' else 'updated'
def send_task():
"""Отложенная отправка задачи после commit транзакции"""
try:
process_request_notifications_task.delay(
request_id=request_obj.id,
action=action
)
except Exception as e:
logger.warning(
f"Failed to queue status change notifications task: {e}, "
f"falling back to sync"
)
# Fallback на старую логику
notify_status_change(request_obj, old_status, new_status)
transaction.on_commit(send_task)
except Exception as e:
logger.exception(f"[SIGNAL ERROR] create_request_notifications: {e}")
print(f"❌ [SIGNAL ERROR] create_request_notifications: {e}")
@receiver(pre_save, sender=Request)
def track_status_change(sender, instance, **kwargs):
"""
Сохраняем старый статус перед обновлением для отслеживания изменений.
"""
if instance.pk:
try:
old_instance = Request.objects.get(pk=instance.pk)
instance._old_status = old_instance.status
except Request.DoesNotExist:
instance._old_status = None
@receiver(m2m_changed, sender=Request.recipients.through)
def notify_on_recipients_changed(sender, instance, action, **kwargs):
"""
Отправляет уведомления о новом заявлении ПОСЛЕ установки recipients.
Срабатывает когда recipients устанавливаются через .set(), .add() и т.д.
"""
try:
from notifications.tasks import process_request_notifications_task
# Проверяем что это завершение операции установки recipients
if action == "post_add" and instance.id in _pending_new_requests:
print(
f"🎯 [M2M_SIGNAL] Recipients установлены для заявления #{instance.id}, отправляем уведомления"
)
_pending_new_requests.discard(instance.id)
def send_task():
"""Отложенная отправка задачи после commit транзакции"""
try:
process_request_notifications_task.delay(
request_id=instance.id,
action='created'
)
except Exception as e:
logger.warning(
f"Failed to queue new request notifications task: {e}, "
f"falling back to sync"
)
# Fallback на старую логику
notify_new_request(instance)
transaction.on_commit(send_task)
except Exception as e:
logger.exception(f"[SIGNAL ERROR] notify_on_recipients_changed: {e}")
print(f"❌ [SIGNAL ERROR] notify_on_recipients_changed: {e}")
@receiver(m2m_changed, sender=Request.cc_users.through)
def notify_on_cc_users_changed(sender, instance, action, **kwargs):
"""
Дополнительная проверка для cc_users.
Если recipients не были установлены, но установлены cc_users,
также отправляем уведомления.
"""
try:
from notifications.tasks import process_request_notifications_task
# Если это завершение добавления cc_users И заявление все еще ожидает уведомлений
if action == "post_add" and instance.id in _pending_new_requests:
# Проверяем что recipients пусты (значит уведомления еще не отправлены)
if instance.recipients.count() == 0:
print(
f"🎯 [M2M_SIGNAL] CC users установлены для заявления #{instance.id} (без recipients), отправляем уведомления"
)
_pending_new_requests.discard(instance.id)
def send_task():
"""Отложенная отправка задачи после commit транзакции"""
try:
process_request_notifications_task.delay(
request_id=instance.id,
action='created'
)
except Exception as e:
logger.warning(
f"Failed to queue new request notifications task: {e}, "
f"falling back to sync"
)
# Fallback на старую логику
notify_new_request(instance)
transaction.on_commit(send_task)
except Exception as e:
logger.exception(f"[SIGNAL ERROR] notify_on_cc_users_changed: {e}")
print(f"❌ [SIGNAL ERROR] notify_on_cc_users_changed: {e}")
@receiver(post_save, sender=RequestComment)
def create_comment_notification(sender, instance, created, **kwargs):
"""
Создает уведомления при добавлении комментария к заявлению асинхронно.
Уведомляет:
- Автора заявления
- Всех получателей
- Всех в копии
- Согласующего
- Сотрудников отделов (если sent_to_all_department)
"""
if not created:
return
from notifications.tasks import process_request_notifications_task
def send_task():
"""Отложенная отправка задачи после commit транзакции"""
try:
process_request_notifications_task.delay(
request_id=instance.request.id,
action='comment',
comment_id=instance.id
)
except Exception as e:
logger.warning(
f"Failed to queue comment notifications task: {e}, "
f"falling back to sync"
)
# Fallback на старую логику
notify_request_comment(instance)
transaction.on_commit(send_task)
author = instance.author
recipients_set = set()
request_obj = instance.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:
NotificationService.create_notification_async(
recipient=recipient,
notification_type_code="request_comment",
title=f"💬 Новый комментарий к заявлению от {employee_name}",
message=(
f"{author_name} прокомментировал заявление "
f'"{request_type}": {instance.text[:100]}'
),
content_object=request_obj,
action_url=f"/requests/{request_obj.id}/",
metadata={
"request_id": request_obj.id,
"request_type": request_obj.type,
"comment_id": instance.id,
"author_id": author.id,
},
)
# ===== Вспомогательные функции =====
def notify_new_request(request_obj):
"""
Отправляет уведомление о новом заявлении:
- Всем основным получателям (recipients) - адресованное им
- Всем в копии (cc_users) - с пометкой "в копии"
- Согласующему (approver)
- Руководителям отделов
- Пользователям с правом can_process_requests
При sent_to_all_department=True отправляет всем сотрудникам отделов
"""
logger.info(
f"\n{'=' * 80}\n"
f"[notify_new_request] 📨 НАЧАЛО ОТПРАВКИ УВЕДОМЛЕНИЙ О НОВОМ ЗАЯВЛЕНИИ\n"
f" Request ID: {request_obj.id}\n"
f" Тип: {request_obj.get_type_display()}\n"
f" Сотрудник: {request_obj.employee}\n"
f" Статус: {request_obj.status}\n"
f"{'=' * 80}"
)
print(f"📨 [notify_new_request] Начинаем обработку заявления #{request_obj.id}")
recipients_set = set()
# 1. Основные получатели
recipients_count = request_obj.recipients.count()
logger.info(
f"[notify_new_request] Основные получатели (recipients): {recipients_count}"
)
print(f" Recipients в БД: {recipients_count}")
for recipient in request_obj.recipients.filter(is_active=True):
recipients_set.add(recipient)
logger.info(
f"[notify_new_request] ✅ Основной получатель: {recipient.username} (ID={recipient.id}, email={recipient.email})"
)
print(
f" ✅ Добавлен основной получатель: {recipient.username} (ID: {recipient.id})"
)
# 2. Копия (CC)
cc_count = request_obj.cc_users.count()
logger.info(f"[notify_new_request] CC users: {cc_count}")
print(f" CC users в БД: {cc_count}")
for cc_user in request_obj.cc_users.filter(is_active=True):
recipients_set.add(cc_user)
logger.info(
f"[notify_new_request] ✅ CC получатель: {cc_user.username} (ID={cc_user.id}, email={cc_user.email})"
)
print(f" ✅ Добавлен CC: {cc_user.username} (ID: {cc_user.id})")
# 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)
# 6. Пользователи с правом обрабатывать заявки в этих отделах
dept_ids = list(request_obj.departments.values_list("id", flat=True))
if request_obj.department_id and request_obj.department_id not in dept_ids:
dept_ids.append(request_obj.department_id)
if dept_ids:
dept_processors = (
Employee.objects.filter(
departments_links__department_id__in=dept_ids,
departments_links__is_active=True,
departments_links__role__scoped_permissions__code="can_process_requests",
is_active=True,
)
.exclude(id=request_obj.employee.id)
.distinct()
)
recipients_set.update(dept_processors)
# Итоговое логирование
logger.info(f"[notify_new_request] 📊 ИТОГО получателей: {len(recipients_set)}")
print(
f"📊 [notify_new_request] Всего получателей для заявления #{request_obj.id}: {len(recipients_set)}"
)
if len(recipients_set) == 0:
logger.warning(
f"[notify_new_request] ⚠️ НЕТ ПОЛУЧАТЕЛЕЙ! Уведомления не отправлены для request_id={request_obj.id}"
)
print(
f"⚠️ [notify_new_request] НЕТ ПОЛУЧАТЕЛЕЙ! Уведомления не будут отправлены."
)
return
for r in recipients_set:
logger.info(
f"[notify_new_request] 👤 {r.username} (ID={r.id}, email={r.email})"
)
print(f" 👤 {r.username} (ID: {r.id})")
# Определяем тип уведомления для каждого получателя
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 "Без комментария"
)
logger.info(
f"[notify_new_request] Начало отправки {len(recipients_set)} уведомлений..."
)
for recipient in recipients_set:
# Определяем роль получателя
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
logger.info(
f"[notify_new_request] 📤 Отправка для {recipient.username}: "
f"primary={is_primary}, cc={is_cc}, approver={is_approver}"
)
print(
f" 📤 Отправка уведомления для {recipient.username}: "
f"primary={is_primary}, cc={is_cc}, approver={is_approver}"
)
# Формируем заголовок и сообщение с учетом роли
if is_primary:
# Основной получатель - требует действия
title = f"📩 Вам адресовано заявление от {author_name}"
message = f'Тип: "{request_type}". {comment_preview}'
notification_type = "request_new" # Используем общий тип
elif is_cc:
# Копия - информирование
title = f"📋 Вы в копии заявления от {author_name}"
message = f'Тип: "{request_type}". {comment_preview}'
notification_type = "request_new" # Используем общий тип
elif is_approver:
title = f"✅ Новое заявление на согласование от {author_name}"
message = f'Тип: "{request_type}". {comment_preview}'
notification_type = "request_new" # Используем общий тип
else:
# Руководитель отдела или обработчик
title = f"📝 Новое заявление в отделе от {author_name}"
message = f'Тип: "{request_type}". {comment_preview}'
notification_type = "request_new" # Используем общий тип
logger.info(
f"[notify_new_request] ➡️ Вызов NotificationService.create_notification_async для {recipient.username}"
)
NotificationService.create_notification_async(
recipient=recipient,
notification_type_code=notification_type,
title=title,
message=message,
content_object=request_obj,
action_url=f"/requests/{request_obj.id}/",
metadata={
"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 (тот кто принял решение) не получает уведомление о своем же решении
"""
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:
if new_status == "approved":
notification_type = "request_approved"
title = f"✅ Заявление одобрено: {request_type}"
message = (
f'Заявление от {employee_name} "{request_type}" '
f"одобрено пользователем {approver_name}"
)
elif new_status == "rejected":
notification_type = "request_rejected"
title = f"❌ Заявление отклонено: {request_type}"
message = (
f'Заявление от {employee_name} "{request_type}" '
f"отклонено пользователем {approver_name}"
)
else:
# Общее уведомление об изменении статуса
notification_type = "request_status_changed"
title = f"🔄 Статус заявления изменен: {request_type}"
message = (
f'Статус заявления от {employee_name} "{request_type}" '
f"изменен: {old_status} → {new_status}"
)
NotificationService.create_notification_async(
recipient=recipient,
notification_type_code=notification_type,
title=title,
message=message,
content_object=request_obj,
action_url=f"/requests/{request_obj.id}/",
metadata={
"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,
},
)