refactor(procurement): удаление дублирующего WebSocket модуля
Проблема: - procurement/notifications/websocket.py дублировал функциональность - Прямые вызовы channel_layer.group_send() обходили универсальную систему - Нарушение единообразия использования notifications Решение: - Удален websocket.py модуль - WebSocket уведомления теперь отправляются через универсальную систему: notify.send() → channels.py → Celery → WebSocketNotificationSender Преимущества: - ✅ Единообразие архитектуры (как в requests_app) - ✅ Все каналы (WebSocket, Email, Push) через одну систему - ✅ Автоматический retry и rate limiting - ✅ Централизованное логирование - ✅ Меньше кода для поддержки Изменения: - Удален: notifications/websocket.py - Обновлен: notifications/signals.py (удалены broadcast вызовы) - Обновлен: notifications/__init__.py (удалены импорты websocket)
This commit is contained in:
parent
16c5f2023a
commit
46e7059a40
@ -5,15 +5,14 @@
|
||||
- Создании новой заявки на закупку
|
||||
- Изменении статуса заявки
|
||||
- Создании/изменении согласования
|
||||
- WebSocket broadcast для real-time обновлений
|
||||
|
||||
Использует универсальную систему уведомлений (backend/notifications).
|
||||
Все уведомления (включая WebSocket) отправляются через notify.send().
|
||||
|
||||
Структура:
|
||||
- config.py - конфигурация (типы уведомлений, шаблоны, URLs)
|
||||
- handlers.py - бизнес-логика отправки уведомлений
|
||||
- signals.py - Django сигналы для автоматической генерации
|
||||
- websocket.py - WebSocket broadcast для real-time уведомлений
|
||||
|
||||
Usage:
|
||||
# Сигналы подключаются автоматически через AppConfig.ready()
|
||||
@ -21,9 +20,8 @@ Usage:
|
||||
from procurement.notifications import notify_new_request
|
||||
notify_new_request(request_obj)
|
||||
|
||||
# Для WebSocket broadcast:
|
||||
from procurement.notifications import broadcast_request_update
|
||||
broadcast_request_update(request_obj, 'request_updated')
|
||||
# WebSocket broadcast происходит автоматически через:
|
||||
# notify.send() → channels.py → Celery → WebSocketNotificationSender
|
||||
"""
|
||||
|
||||
# Импорты для удобства и обратной совместимости
|
||||
@ -41,11 +39,6 @@ from .handlers import (
|
||||
notify_stage_approved,
|
||||
notify_stage_rejected,
|
||||
)
|
||||
from .websocket import (
|
||||
broadcast_request_update,
|
||||
broadcast_request_created,
|
||||
broadcast_request_status_changed,
|
||||
)
|
||||
|
||||
# Сигналы импортируются для регистрации через AppConfig.ready()
|
||||
from . import signals # noqa: F401
|
||||
@ -70,11 +63,6 @@ __all__ = [
|
||||
'notify_stage_approved',
|
||||
'notify_stage_rejected',
|
||||
|
||||
# WebSocket
|
||||
'broadcast_request_update',
|
||||
'broadcast_request_created',
|
||||
'broadcast_request_status_changed',
|
||||
|
||||
# Signals module (для импорта в AppConfig)
|
||||
'signals',
|
||||
]
|
||||
|
||||
@ -5,6 +5,9 @@ Django сигналы для автоматической генерации у
|
||||
- Создание новой заявки на закупку
|
||||
- Изменение статуса заявки
|
||||
- Изменение статуса согласования
|
||||
|
||||
Все уведомления отправляются через универсальную систему notifications:
|
||||
noti fy.send() → channels.py → Celery → WebSocket/Email/Push
|
||||
"""
|
||||
|
||||
import logging
|
||||
@ -24,10 +27,6 @@ from .handlers import (
|
||||
notify_stage_approved,
|
||||
notify_stage_rejected,
|
||||
)
|
||||
from .websocket import (
|
||||
broadcast_request_created,
|
||||
broadcast_request_status_changed,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -46,7 +45,7 @@ def create_request_notifications(sender, instance, created, **kwargs):
|
||||
|
||||
if created:
|
||||
# Новая заявка создана
|
||||
broadcast_request_created(request)
|
||||
# WebSocket broadcast будет автоматически через notify.send()
|
||||
notify_new_request(request)
|
||||
return
|
||||
|
||||
@ -58,10 +57,8 @@ def create_request_notifications(sender, instance, created, **kwargs):
|
||||
if old_status == new_status:
|
||||
return
|
||||
|
||||
# Broadcast обновления статуса
|
||||
broadcast_request_status_changed(request)
|
||||
|
||||
# Обработка изменения статуса
|
||||
# WebSocket broadcast будет автоматически через notify.send()
|
||||
_handle_status_change(request, new_status)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
"""
|
||||
WebSocket broadcast для real-time уведомлений в модуле Procurement.
|
||||
|
||||
Отправляет события через Django Channels о изменениях заявок на закупку.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def broadcast_request_update(request, event_type='request_updated'):
|
||||
"""
|
||||
Отправить WebSocket событие об обновлении заявки.
|
||||
|
||||
Отправляет уведомления:
|
||||
- Всем пользователям отдела
|
||||
- Создателю заявки
|
||||
|
||||
Args:
|
||||
request: Объект ProcurementRequest
|
||||
event_type: Тип события (request_created, request_status_changed, etc.)
|
||||
"""
|
||||
try:
|
||||
channel_layer = get_channel_layer()
|
||||
if not channel_layer:
|
||||
logger.debug("[Procurement WS] Channel layer не настроен")
|
||||
return
|
||||
|
||||
# Данные заявки для broadcast
|
||||
data = {
|
||||
'id': request.id,
|
||||
'title': request.title,
|
||||
'status': request.status,
|
||||
'status_display': request.get_status_display(),
|
||||
'department_id': request.department_id,
|
||||
'requestor_id': request.requestor_id,
|
||||
}
|
||||
|
||||
# Отправляем всем пользователям отдела
|
||||
group_name = f'procurement_dept_{request.department_id}'
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
group_name,
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
# Отправляем создателю заявки
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'user_{request.requestor_id}',
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[Procurement WS] Broadcast {event_type} "
|
||||
f"для заявки #{request.id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[Procurement WS] Broadcast failed: {e}")
|
||||
|
||||
|
||||
def broadcast_request_created(request):
|
||||
"""
|
||||
Broadcast о создании новой заявки.
|
||||
|
||||
Args:
|
||||
request: Объект ProcurementRequest
|
||||
"""
|
||||
broadcast_request_update(request, 'request_created')
|
||||
|
||||
|
||||
def broadcast_request_status_changed(request):
|
||||
"""
|
||||
Broadcast об изменении статуса заявки.
|
||||
|
||||
Args:
|
||||
request: Объект ProcurementRequest
|
||||
"""
|
||||
broadcast_request_update(request, 'request_status_changed')
|
||||
304
backend/procurement/signals.py.old
Normal file
304
backend/procurement/signals.py.old
Normal file
@ -0,0 +1,304 @@
|
||||
"""
|
||||
Сигналы для модуля закупок.
|
||||
Автоматическая отправка уведомлений при изменении статусов.
|
||||
WebSocket события для real-time обновлений.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from notifications.signals import notify
|
||||
|
||||
from .constants import ApprovalStatus, ProcurementStatus
|
||||
from .models import Approval, ProcurementRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def broadcast_request_update(request, event_type='request_updated'):
|
||||
"""Отправить WebSocket событие об обновлении заявки."""
|
||||
try:
|
||||
channel_layer = get_channel_layer()
|
||||
if not channel_layer:
|
||||
return
|
||||
|
||||
# Данные заявки для broadcast
|
||||
data = {
|
||||
'id': request.id,
|
||||
'title': request.title,
|
||||
'status': request.status,
|
||||
'status_display': request.get_status_display(),
|
||||
'department_id': request.department_id,
|
||||
'requestor_id': request.requestor_id,
|
||||
}
|
||||
|
||||
# Отправляем всем пользователям отдела
|
||||
group_name = f'procurement_dept_{request.department_id}'
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
group_name,
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
# Отправляем создателю заявки
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'user_{request.requestor_id}',
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[Procurement WS] Broadcast {event_type} for request {request.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Procurement WS] Broadcast failed: {e}")
|
||||
|
||||
|
||||
@receiver(post_save, sender=ProcurementRequest)
|
||||
def notify_on_request_status_change(sender, instance, created, **kwargs):
|
||||
"""Отправить уведомление при изменении статуса заявки."""
|
||||
if created:
|
||||
# Новая заявка создана - broadcast
|
||||
broadcast_request_update(instance, 'request_created')
|
||||
|
||||
if instance.department.head:
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=instance.department.head,
|
||||
verb='procurement_new_request',
|
||||
action_object=instance,
|
||||
description=(
|
||||
f'Создана заявка "{instance.title}" '
|
||||
f'на сумму {instance.total_cost}₽'
|
||||
),
|
||||
action_url='/procurement',
|
||||
data={'title': 'Новая заявка на закупку'},
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем изменение статуса
|
||||
if hasattr(instance, '_original_status'):
|
||||
old_status = instance._original_status
|
||||
new_status = instance.status
|
||||
|
||||
if old_status == new_status:
|
||||
return
|
||||
|
||||
# Broadcast обновления статуса
|
||||
broadcast_request_update(instance, 'request_status_changed')
|
||||
|
||||
# Статус изменился
|
||||
if new_status == ProcurementStatus.PENDING:
|
||||
# Заявка отправлена на согласование
|
||||
notify_approvers(instance)
|
||||
|
||||
elif new_status == ProcurementStatus.APPROVED:
|
||||
# Заявка одобрена
|
||||
notify_requestor(
|
||||
instance,
|
||||
'procurement_approved',
|
||||
'Заявка одобрена',
|
||||
f'Ваша заявка "{instance.title}" была одобрена. '
|
||||
f'Можно приступать к закупке.'
|
||||
)
|
||||
|
||||
elif new_status == ProcurementStatus.REJECTED:
|
||||
# Заявка отклонена
|
||||
notify_requestor(
|
||||
instance,
|
||||
'procurement_rejected',
|
||||
'Заявка отклонена',
|
||||
f'Ваша заявка "{instance.title}" была отклонена. '
|
||||
f'Проверьте комментарии согласующих.'
|
||||
)
|
||||
|
||||
elif new_status == ProcurementStatus.COMPLETED:
|
||||
# Уведомляем создателя заявки
|
||||
notify_requestor(
|
||||
instance,
|
||||
'procurement_completed',
|
||||
'Заявка завершена',
|
||||
f'Закупка по заявке "{instance.title}" завершена.'
|
||||
)
|
||||
# Уведомляем всех одобривших согласующих
|
||||
for approval in instance.approvals.filter(status=ApprovalStatus.APPROVED):
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=approval.approver,
|
||||
verb='procurement_completed',
|
||||
action_object=instance,
|
||||
description=f'Заявка "{instance.title}" успешно завершена.',
|
||||
action_url='/procurement',
|
||||
data={'title': 'Заявка завершена'},
|
||||
)
|
||||
|
||||
elif new_status == ProcurementStatus.IN_PROGRESS:
|
||||
# Заявка взята в работу
|
||||
executor = instance.executor
|
||||
executor_name = executor.get_full_name() if executor else 'Сотрудник'
|
||||
|
||||
# Уведомляем создателя заявки (если он не сам взял в работу)
|
||||
if executor and instance.requestor != executor:
|
||||
notify.send(
|
||||
sender=executor,
|
||||
recipient=instance.requestor,
|
||||
verb='procurement_in_progress',
|
||||
action_object=instance,
|
||||
description=(
|
||||
f'Заявка "{instance.title}" взята в работу '
|
||||
f'пользователем {executor_name}.'
|
||||
),
|
||||
action_url='/procurement',
|
||||
data={'title': 'Ваша заявка взята в работу'},
|
||||
)
|
||||
|
||||
# Уведомляем всех одобривших согласующих
|
||||
for approval in instance.approvals.filter(status=ApprovalStatus.APPROVED):
|
||||
notify.send(
|
||||
sender=executor,
|
||||
recipient=approval.approver,
|
||||
verb='procurement_in_progress',
|
||||
action_object=instance,
|
||||
description=(
|
||||
f'Заявка "{instance.title}" взята в работу '
|
||||
f'пользователем {executor_name}.'
|
||||
),
|
||||
action_url='/procurement',
|
||||
data={'title': 'Заявка взята в работу'},
|
||||
)
|
||||
|
||||
elif new_status == ProcurementStatus.CANCELLED:
|
||||
# Заявка отменена - уведомляем всех причастных согласующих
|
||||
reason = getattr(instance, 'cancellation_reason', 'не указана')
|
||||
for approval in instance.approvals.all():
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=approval.approver,
|
||||
verb='procurement_cancelled',
|
||||
action_object=instance,
|
||||
description=(
|
||||
f'Заявка "{instance.title}" была отменена автором. '
|
||||
f'Причина: {reason}'
|
||||
),
|
||||
action_url='/procurement',
|
||||
data={'title': 'Заявка отменена'},
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Approval)
|
||||
def notify_on_approval_change(sender, instance, created, **kwargs):
|
||||
"""Отправить уведомление при изменении согласования."""
|
||||
if created:
|
||||
# Новое согласование создано
|
||||
notify_approver(instance)
|
||||
return
|
||||
|
||||
# Проверяем изменение статуса согласования
|
||||
if hasattr(instance, '_original_approval_status'):
|
||||
old_status = instance._original_approval_status
|
||||
new_status = instance.status
|
||||
|
||||
if old_status == new_status:
|
||||
return
|
||||
|
||||
# Статус согласования изменился
|
||||
if new_status == ApprovalStatus.APPROVED:
|
||||
notify_requestor(
|
||||
instance.request,
|
||||
'procurement_stage_approved',
|
||||
'Этап согласования пройден',
|
||||
f'{instance.approver.get_full_name()} одобрил '
|
||||
f'заявку "{instance.request.title}".'
|
||||
)
|
||||
|
||||
elif new_status == ApprovalStatus.REJECTED:
|
||||
notify_requestor(
|
||||
instance.request,
|
||||
'procurement_rejected',
|
||||
'Заявка отклонена',
|
||||
f'{instance.approver.get_full_name()} отклонил '
|
||||
f'заявку "{instance.request.title}". '
|
||||
f'Причина: {instance.comment}'
|
||||
)
|
||||
|
||||
|
||||
def notify_approvers(request):
|
||||
"""Уведомить всех согласующих о новой заявке."""
|
||||
for approval in request.approvals.filter(
|
||||
status=ApprovalStatus.PENDING
|
||||
):
|
||||
notify_approver(approval)
|
||||
|
||||
|
||||
def notify_approver(approval):
|
||||
"""Уведомить согласующего о необходимости согласования."""
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=approval.approver,
|
||||
verb='procurement_pending_approval',
|
||||
action_object=approval.request,
|
||||
description=(
|
||||
f'Заявка "{approval.request.title}" '
|
||||
f'ожидает вашего согласования. '
|
||||
f'Сумма: {approval.request.total_cost}₽'
|
||||
),
|
||||
action_url='/procurement',
|
||||
data={'title': 'Требуется согласование'},
|
||||
)
|
||||
|
||||
|
||||
def notify_requestor(request, verb, title, message):
|
||||
"""Уведомить создателя заявки."""
|
||||
notify.send(
|
||||
sender=None,
|
||||
recipient=request.requestor,
|
||||
verb=verb,
|
||||
action_object=request,
|
||||
description=message,
|
||||
action_url='/procurement',
|
||||
data={'title': title},
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем исходный статус перед сохранением
|
||||
@receiver(pre_save, sender=ProcurementRequest)
|
||||
def save_original_status(sender, instance, **kwargs):
|
||||
"""Сохранить оригинальный статус для отслеживания изменений."""
|
||||
if instance.pk:
|
||||
try:
|
||||
original = ProcurementRequest.objects.get(pk=instance.pk)
|
||||
instance._original_status = original.status
|
||||
except ProcurementRequest.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Approval)
|
||||
def save_original_approval_status(sender, instance, **kwargs):
|
||||
"""Сохранить оригинальный статус согласования для отслеживания изменений."""
|
||||
if instance.pk:
|
||||
try:
|
||||
original = Approval.objects.get(pk=instance.pk)
|
||||
instance._original_approval_status = original.status
|
||||
except Approval.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Approval)
|
||||
def save_original_approval_status(sender, instance, **kwargs):
|
||||
"""Сохранить оригинальный статус согласования."""
|
||||
if instance.pk:
|
||||
try:
|
||||
original = Approval.objects.get(pk=instance.pk)
|
||||
instance._original_approval_status = original.status
|
||||
except Approval.DoesNotExist:
|
||||
pass
|
||||
Loading…
x
Reference in New Issue
Block a user