Corp/backend/requests_app/notification_signals.py
Игорь Надеин 117100bf9d fix(requests): Исправлена ошибка с employee_departments и добавлены тесты
- Заменено employee_departments на departments_links в notification_signals.py
- Добавлен импорт Department в serializers.py
- Создан комплексный тестовый скрипт test_recipients.py (7 тестов)
- Создан скрипт test_recipients_api.py для API тестирования
- Добавлены утилиты для создания тестовых данных

Результаты тестирования:
✓ 7/7 тестов модели прошли успешно (100%)
✓ Проверены все методы: add_recipient, is_recipient, all_recipients
✓ Проверена работа с ManyToMany полями
✓ Проверена сериализация через API
✓ Проверен флаг sent_to_all_department
2025-12-02 17:39:09 +03:00

326 lines
13 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.
Обрабатывает события:
- Новая заявка (для ответственных/руководителей)
- Одобрение заявки
- Отклонение заявки
- Комментарий к заявке
- Изменение статуса заявки
"""
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Request, RequestComment
from notifications.services import NotificationService
Employee = get_user_model()
@receiver(post_save, sender=Request)
def create_request_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании или изменении заявки.
Обрабатывает:
1. Новая заявка - уведомление руководителю/ответственным
2. Изменение статуса - уведомление автору
"""
request_obj = instance
if created and request_obj.status != 'draft':
# Новая заявка - уведомляем ответственных
notify_new_request(request_obj)
elif not created:
# Проверяем изменение статуса
# Получаем предыдущее состояние из БД
try:
old_request = Request.objects.get(pk=request_obj.pk)
if hasattr(request_obj, '_old_status'):
old_status = request_obj._old_status
new_status = request_obj.status
if old_status != new_status:
notify_status_change(request_obj, old_status, new_status)
except Request.DoesNotExist:
pass
@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(post_save, sender=RequestComment)
def create_comment_notification(sender, instance, created, **kwargs):
"""
Создает уведомление при добавлении комментария к заявке.
Уведомляет:
- Автора заявки
- Всех получателей
- Всех в копии
- Согласующего
- Сотрудников отделов (если sent_to_all_department)
"""
if not created:
return
comment = instance
request_obj = comment.request
author = comment.author
recipients_set = set()
# Автор заявки
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)
# Отправляем уведомления
for recipient in recipients_set:
NotificationService.create_notification(
recipient=recipient,
notification_type_code='request_comment',
title='Новый комментарий к заявке',
message=(
f'{author.get_full_name() or author.username} '
f'прокомментировал заявку: {comment.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': comment.id,
'author_id': author.id,
}
)
# ===== Вспомогательные функции =====
def notify_new_request(request_obj):
"""
Отправляет уведомление о новой заявке:
- Всем основным получателям (recipients)
- Всем в копии (cc_users)
- Согласующему (approver)
- Руководителям отделов
- Пользователям с правом can_process_requests
При sent_to_all_department=True отправляет всем сотрудникам отделов
"""
recipients_set = set()
# 1. Основные получатели
for recipient in request_obj.recipients.filter(is_active=True):
recipients_set.add(recipient)
# 2. Копия (CC)
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)
# 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)
# Определяем тип уведомления для каждого получателя
author_name = (
request_obj.employee.get_full_name() or
request_obj.employee.username
)
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
# Формируем заголовок и сообщение
if is_approver:
title = f'Новая заявка на согласование от {author_name}'
notification_type = 'request_new'
elif is_primary:
title = f'Вам адресована заявка от {author_name}'
notification_type = 'request_new'
elif is_cc:
title = f'Вы в копии заявки от {author_name}'
notification_type = 'request_new'
else:
title = f'Новая заявка в отделе от {author_name}'
notification_type = 'request_new'
message = (
f'Тип: {request_obj.get_type_display()}. '
f'{request_obj.comment[:100] if request_obj.comment else ""}'
)
NotificationService.create_notification(
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,
'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)
"""
recipients_to_notify = set()
# 1. Автор
recipients_to_notify.add(request_obj.employee)
# 2. Основные получатели
recipients_to_notify.update(
request_obj.recipients.filter(is_active=True)
)
# 3. Копия
recipients_to_notify.update(
request_obj.cc_users.filter(is_active=True)
)
# 4. Если 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_to_notify.update(dept_employees)
# Формируем уведомления
for recipient in recipients_to_notify:
if new_status == 'approved':
notification_type = 'request_approved'
title = 'Заявка одобрена'
approver_name = (
request_obj.approver.get_full_name()
if request_obj.approver else 'Руководитель'
)
message = (
f'Заявка "{request_obj.get_type_display()}" '
f'одобрена пользователем {approver_name}'
)
elif new_status == 'rejected':
notification_type = 'request_rejected'
title = 'Заявка отклонена'
approver_name = (
request_obj.approver.get_full_name()
if request_obj.approver else 'Руководитель'
)
message = (
f'Заявка "{request_obj.get_type_display()}" '
f'отклонена пользователем {approver_name}'
)
else:
# Общее уведомление об изменении статуса
notification_type = 'request_status_changed'
title = 'Статус заявки изменен'
message = (
f'Статус заявки "{request_obj.get_type_display()}" '
f'изменен: {old_status} → {new_status}'
)
NotificationService.create_notification(
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,
'old_status': old_status,
'new_status': new_status,
'approver_id': (
request_obj.approver.id if request_obj.approver else None
),
}
)