refactor(requests_app): реструктуризация notifications в модульную архитектуру
- Создана папка backend/requests_app/notifications/ вместо одного файла - Разделена логика на модули: * constants.py - типы уведомлений (verbs) и шаблоны сообщений * handlers.py - бизнес-логика отправки уведомлений * signals.py - Django сигналы для автоматической генерации * __init__.py - публичный API модуля Преимущества новой структуры: - ✅ Лучшая организация кода (SRP - Single Responsibility Principle) - ✅ Легче поддерживать и расширять - ✅ Константы в одном месте (NotificationVerbs, MessageTemplates) - ✅ Переиспользуемые шаблоны сообщений - ✅ Обратная совместимость через __init__.py Удален: notification_signals.py (сохранен как .old для истории) Добавлен: notifications/ модуль с 4 файлами
This commit is contained in:
parent
a5f61b48c9
commit
d83aae6791
@ -6,6 +6,6 @@ class RequestsAppConfig(AppConfig):
|
||||
name = 'requests_app'
|
||||
|
||||
def ready(self):
|
||||
# Подключаем signals для уведомлений
|
||||
import requests_app.notification_signals # noqa
|
||||
# Подключаем модуль уведомлений (signals регистрируются автоматически)
|
||||
import requests_app.notifications.signals # noqa
|
||||
import requests_app.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
@ -1,516 +0,0 @@
|
||||
"""
|
||||
Signals для автоматической генерации уведомлений в модуле Requests.
|
||||
|
||||
Обрабатывает события:
|
||||
- Новое заявление (для ответственных/руководителей)
|
||||
- Одобрение заявления
|
||||
- Отклонение заявления
|
||||
- Комментарий к заявлению
|
||||
- Изменение статуса заявления
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
from notifications.signals import notify
|
||||
|
||||
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:
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_status_change(request_obj, old_status, new_status)
|
||||
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:
|
||||
# Проверяем что это завершение операции установки 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)
|
||||
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_new_request(instance)
|
||||
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:
|
||||
# Если это завершение добавления 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)
|
||||
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_new_request(instance)
|
||||
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)
|
||||
|
||||
notify.send() → channels.py автоматически отправит через Celery
|
||||
"""
|
||||
if not created:
|
||||
return
|
||||
|
||||
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:
|
||||
# Не отправляем уведомление тому, кто сам написал комментарий
|
||||
if recipient.id == author.id:
|
||||
continue
|
||||
|
||||
notify.send(
|
||||
sender=author,
|
||||
recipient=recipient,
|
||||
verb='request_comment',
|
||||
action_object=request_obj,
|
||||
description=(
|
||||
f"{author_name} прокомментировал заявление "
|
||||
f'"{request_type}": {instance.text[:100]}'
|
||||
),
|
||||
action_url='/requests',
|
||||
data={
|
||||
'title': f'💬 Новый комментарий к заявлению от {employee_name}',
|
||||
'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] ➡️ Отправка notify.send для {recipient.username}"
|
||||
)
|
||||
|
||||
notify.send(
|
||||
sender=request_obj.employee,
|
||||
recipient=recipient,
|
||||
verb=notification_type,
|
||||
action_object=request_obj,
|
||||
description=message,
|
||||
action_url='/requests',
|
||||
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 (тот кто принял решение) не получает уведомление о своем же решении
|
||||
"""
|
||||
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}'
|
||||
)
|
||||
|
||||
notify.send(
|
||||
sender=request_obj.approver if request_obj.approver else None,
|
||||
recipient=recipient,
|
||||
verb=notification_type,
|
||||
action_object=request_obj,
|
||||
description=message,
|
||||
action_url='/requests',
|
||||
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,
|
||||
},
|
||||
)
|
||||
48
backend/requests_app/notifications/__init__.py
Normal file
48
backend/requests_app/notifications/__init__.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
Модуль уведомлений для приложения Requests.
|
||||
|
||||
Обеспечивает автоматическую отправку уведомлений при:
|
||||
- Создании нового заявления
|
||||
- Изменении статуса заявления
|
||||
- Добавлении комментария к заявлению
|
||||
|
||||
Использует универсальную систему уведомлений (backend/notifications).
|
||||
|
||||
Структура:
|
||||
- constants.py - константы (типы уведомлений, шаблоны сообщений)
|
||||
- handlers.py - бизнес-логика отправки уведомлений
|
||||
- signals.py - Django сигналы для автоматической генерации
|
||||
|
||||
Usage:
|
||||
# Сигналы подключаются автоматически через AppConfig.ready()
|
||||
# Для ручной отправки:
|
||||
from requests_app.notifications import notify_new_request
|
||||
notify_new_request(request_obj)
|
||||
"""
|
||||
|
||||
# Импорты для удобства и обратной совместимости
|
||||
from .constants import NotificationVerbs, MessageTemplates, ActionURLs
|
||||
from .handlers import (
|
||||
notify_new_request,
|
||||
notify_status_change,
|
||||
notify_comment,
|
||||
)
|
||||
|
||||
# Сигналы импортируются для регистрации через AppConfig.ready()
|
||||
from . import signals # noqa: F401
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Constants
|
||||
'NotificationVerbs',
|
||||
'MessageTemplates',
|
||||
'ActionURLs',
|
||||
|
||||
# Handlers
|
||||
'notify_new_request',
|
||||
'notify_status_change',
|
||||
'notify_comment',
|
||||
|
||||
# Signals module (для импорта в AppConfig)
|
||||
'signals',
|
||||
]
|
||||
171
backend/requests_app/notifications/constants.py
Normal file
171
backend/requests_app/notifications/constants.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""
|
||||
Константы для уведомлений модуля Requests.
|
||||
|
||||
Определяет типы уведомлений (verbs) и шаблоны сообщений.
|
||||
"""
|
||||
|
||||
|
||||
# ===== Типы уведомлений (Verbs) =====
|
||||
|
||||
class NotificationVerbs:
|
||||
"""Типы уведомлений для заявлений."""
|
||||
|
||||
# Новое заявление (универсальный для всех ролей получателей)
|
||||
REQUEST_NEW = 'request_new'
|
||||
|
||||
# Комментарий к заявлению
|
||||
REQUEST_COMMENT = 'request_comment'
|
||||
|
||||
# Изменение статуса
|
||||
REQUEST_APPROVED = 'request_approved'
|
||||
REQUEST_REJECTED = 'request_rejected'
|
||||
REQUEST_STATUS_CHANGED = 'request_status_changed'
|
||||
|
||||
|
||||
# ===== Шаблоны сообщений =====
|
||||
|
||||
class MessageTemplates:
|
||||
"""Шаблоны сообщений для уведомлений."""
|
||||
|
||||
@staticmethod
|
||||
def new_request_primary(
|
||||
author_name: str, request_type: str, comment_preview: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для основных получателей заявления.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📩 Вам адресовано заявление от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def new_request_cc(
|
||||
author_name: str, request_type: str, comment_preview: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для получателей в копии.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📋 Вы в копии заявления от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def new_request_approver(
|
||||
author_name: str, request_type: str, comment_preview: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для согласующего.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"✅ Новое заявление на согласование от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def new_request_department(
|
||||
author_name: str, request_type: str, comment_preview: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для руководителей отдела и обработчиков.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f"📝 Новое заявление в отделе от {author_name}"
|
||||
message = f'Тип: "{request_type}". {comment_preview}'
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def comment(
|
||||
author_name: str,
|
||||
request_type: str,
|
||||
employee_name: str,
|
||||
comment_text: str,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для комментария.
|
||||
|
||||
Returns:
|
||||
tuple: (title, description)
|
||||
"""
|
||||
title = f'💬 Новый комментарий к заявлению от {employee_name}'
|
||||
description = (
|
||||
f'{author_name} прокомментировал заявление '
|
||||
f'"{request_type}": {comment_text[:100]}'
|
||||
)
|
||||
return title, description
|
||||
|
||||
@staticmethod
|
||||
def status_approved(
|
||||
employee_name: str, request_type: str, approver_name: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для одобрения заявления.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f'✅ Заявление одобрено: {request_type}'
|
||||
message = (
|
||||
f'Заявление от {employee_name} "{request_type}" '
|
||||
f'одобрено пользователем {approver_name}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def status_rejected(
|
||||
employee_name: str, request_type: str, approver_name: str
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для отклонения заявления.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f'❌ Заявление отклонено: {request_type}'
|
||||
message = (
|
||||
f'Заявление от {employee_name} "{request_type}" '
|
||||
f'отклонено пользователем {approver_name}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
@staticmethod
|
||||
def status_changed(
|
||||
employee_name: str,
|
||||
request_type: str,
|
||||
old_status: str,
|
||||
new_status: str,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Шаблон для изменения статуса.
|
||||
|
||||
Returns:
|
||||
tuple: (title, message)
|
||||
"""
|
||||
title = f'🔄 Статус заявления изменен: {request_type}'
|
||||
message = (
|
||||
f'Статус заявления от {employee_name} "{request_type}" '
|
||||
f'изменен: {old_status} → {new_status}'
|
||||
)
|
||||
return title, message
|
||||
|
||||
|
||||
# ===== URL для действий =====
|
||||
|
||||
class ActionURLs:
|
||||
"""URL для переходов из уведомлений."""
|
||||
|
||||
REQUESTS_LIST = '/requests'
|
||||
|
||||
@staticmethod
|
||||
def request_detail(request_id: int) -> str:
|
||||
"""URL детальной страницы заявления."""
|
||||
return f'/requests/{request_id}'
|
||||
462
backend/requests_app/notifications/handlers.py
Normal file
462
backend/requests_app/notifications/handlers.py
Normal file
@ -0,0 +1,462 @@
|
||||
"""
|
||||
Обработчики (handlers) для отправки уведомлений о заявлениях.
|
||||
|
||||
Содержит бизнес-логику формирования и отправки уведомлений
|
||||
для различных событий в модуле Requests.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from notifications.signals import notify
|
||||
|
||||
from .constants import NotificationVerbs, MessageTemplates, ActionURLs
|
||||
|
||||
Employee = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def notify_new_request(request_obj):
|
||||
"""
|
||||
Отправляет уведомление о новом заявлении:
|
||||
- Всем основным получателям (recipients) - адресованное им
|
||||
- Всем в копии (cc_users) - с пометкой "в копии"
|
||||
- Согласующему (approver)
|
||||
- Руководителям отделов
|
||||
- Пользователям с правом can_process_requests
|
||||
|
||||
При sent_to_all_department=True отправляет всем сотрудникам отделов
|
||||
|
||||
Args:
|
||||
request_obj: Объект Request
|
||||
"""
|
||||
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] Начинаем обработку "
|
||||
f"заявления #{request_obj.id}"
|
||||
)
|
||||
|
||||
recipients_set = set()
|
||||
|
||||
# 1. Основные получатели
|
||||
recipients_count = request_obj.recipients.count()
|
||||
logger.info(
|
||||
f"[notify_new_request] Основные получатели: {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] ✅ Основной получатель: "
|
||||
f"{recipient.username} (ID={recipient.id})"
|
||||
)
|
||||
print(
|
||||
f" ✅ Добавлен основной получатель: "
|
||||
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 получатель: "
|
||||
f"{cc_user.username} (ID={cc_user.id})"
|
||||
)
|
||||
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] 📊 ИТОГО получателей: "
|
||||
f"{len(recipients_set)}"
|
||||
)
|
||||
print(
|
||||
f"📊 [notify_new_request] Всего получателей для заявления "
|
||||
f"#{request_obj.id}: {len(recipients_set)}"
|
||||
)
|
||||
|
||||
if len(recipients_set) == 0:
|
||||
logger.warning(
|
||||
f"[notify_new_request] ⚠️ НЕТ ПОЛУЧАТЕЛЕЙ! "
|
||||
f"request_id={request_obj.id}"
|
||||
)
|
||||
print(
|
||||
"⚠️ [notify_new_request] НЕТ ПОЛУЧАТЕЛЕЙ! "
|
||||
"Уведомления не будут отправлены."
|
||||
)
|
||||
return
|
||||
|
||||
for r in recipients_set:
|
||||
logger.info(
|
||||
f"[notify_new_request] 👤 {r.username} (ID={r.id})"
|
||||
)
|
||||
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:
|
||||
_send_new_request_notification(
|
||||
request_obj=request_obj,
|
||||
recipient=recipient,
|
||||
author_name=author_name,
|
||||
request_type=request_type,
|
||||
comment_preview=comment_preview,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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, 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
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[notify_new_request] ➡️ Отправка notify.send для {recipient.username}"
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
159
backend/requests_app/notifications/signals.py
Normal file
159
backend/requests_app/notifications/signals.py
Normal file
@ -0,0 +1,159 @@
|
||||
"""
|
||||
Django сигналы для автоматической генерации уведомлений в модуле Requests.
|
||||
|
||||
Обрабатывает события:
|
||||
- Новое заявление (для ответственных/руководителей)
|
||||
- Одобрение/отклонение заявления
|
||||
- Комментарий к заявлению
|
||||
- Изменение статуса заявления
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from ..models import Request, RequestComment
|
||||
from .handlers import notify_new_request, notify_status_change, notify_comment
|
||||
|
||||
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
|
||||
|
||||
# Логирование для отладки
|
||||
logger.debug(
|
||||
f"[SIGNAL] create_request_notifications: created={created}, "
|
||||
f"status={request_obj.status}, id={request_obj.id}"
|
||||
)
|
||||
|
||||
if created and request_obj.status != "draft":
|
||||
# Помечаем заявление как ожидающее отправки уведомлений
|
||||
# Уведомления будут отправлены после установки recipients через m2m_changed
|
||||
logger.debug(
|
||||
f"[SIGNAL] Помечаем заявление #{request_obj.id} для отправки "
|
||||
f"уведомлений после установки recipients"
|
||||
)
|
||||
_pending_new_requests.add(request_obj.id)
|
||||
|
||||
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:
|
||||
logger.info(
|
||||
f"[SIGNAL] Статус изменен для заявления #{request_obj.id}: "
|
||||
f"{old_status} → {new_status}"
|
||||
)
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_status_change(request_obj, old_status, new_status)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(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:
|
||||
# Проверяем что это завершение операции установки recipients
|
||||
if action == "post_add" and instance.id in _pending_new_requests:
|
||||
logger.info(
|
||||
f"[M2M_SIGNAL] Recipients установлены для заявления #{instance.id}, "
|
||||
f"отправляем уведомления"
|
||||
)
|
||||
_pending_new_requests.discard(instance.id)
|
||||
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_new_request(instance)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(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:
|
||||
# Если это завершение добавления cc_users И заявление все еще ожидает уведомлений
|
||||
if action == "post_add" and instance.id in _pending_new_requests:
|
||||
# Проверяем что recipients пусты (значит уведомления еще не отправлены)
|
||||
if instance.recipients.count() == 0:
|
||||
logger.info(
|
||||
f"[M2M_SIGNAL] CC users установлены для заявления #{instance.id} "
|
||||
f"(без recipients), отправляем уведомления"
|
||||
)
|
||||
_pending_new_requests.discard(instance.id)
|
||||
|
||||
# Отправляем уведомления напрямую - channels.py автоматически
|
||||
# сделает асинхронную отправку через Celery
|
||||
notify_new_request(instance)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(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)
|
||||
|
||||
notify.send() → channels.py автоматически отправит через Celery
|
||||
"""
|
||||
if not created:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"[SIGNAL] Новый комментарий #{instance.id} к заявлению #{instance.request.id}"
|
||||
)
|
||||
notify_comment(instance)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"[SIGNAL ERROR] create_comment_notification: {e}")
|
||||
Loading…
x
Reference in New Issue
Block a user