- Добавлен эндпоинт удаления комментариев к заявкам (DELETE /api/v1/requests/{pk}/comments/{comment_id}/)
- Исправлены references на переменную в сигналах комментариев (comment -> instance)
- Добавлена обработка статуса cancelled для заявок в notification_type_map
- Обновлены процессоры уведомлений в task_base.py (правильные поля моделей)
- Создан скрипт генерации тестовых пользователей (scripts/create_test_users.py)
579 lines
25 KiB
Python
579 lines
25 KiB
Python
"""
|
||
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,
|
||
},
|
||
)
|