- Заменено 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
326 lines
13 KiB
Python
326 lines
13 KiB
Python
"""
|
||
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
|
||
),
|
||
}
|
||
)
|