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:
lizerk 2026-03-10 12:17:07 +03:00
parent a5f61b48c9
commit d83aae6791
6 changed files with 842 additions and 518 deletions

View File

@ -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: регистрация предикатов и правил доступа

View File

@ -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,
},
)

View 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',
]

View 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}'

View 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,
},
)

View 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}")