Corp/backend/documents/notification_signals.py
Игорь Надеин 35f365a1b0 feat: Add department-based document distribution with department picker in UI
- Added departments ManyToMany field to Document model
- Implemented department selection in document create/edit modals
- Added automatic notification creation for all department employees
- Fixed document filtering to include uploaded documents in 'My Documents'
- Fixed acknowledgements API to include department recipients
- Added bulk notification support with configurable threshold
- Removed acknowledge button, added visual highlighting for unread documents
- Implemented server-side acknowledgment flow with redirect to file
- Fixed mobile responsive layout for document actions
- Removed temporary test files and documentation
2025-11-21 12:46:43 +03:00

485 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Signals для автоматической генерации уведомлений в модуле Documents.
Обрабатывает события:
- Новый документ на ознакомление
- Все сотрудники ознакомились
- Напоминание о необходимости ознакомления (через Celery task)
"""
import logging
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Document, DocumentAcknowledgement
from notifications.services import NotificationService
logger = logging.getLogger(__name__)
Employee = get_user_model()
@receiver(post_save, sender=Document)
def create_document_notification(sender, instance, created, **kwargs):
"""
Создает уведомления при создании нового документа.
Уведомления отправляются:
- Всем активным сотрудникам (если sent_to_all=True)
- Сотрудникам выбранных отделов (через m2m_changed signal)
- Выбранным получателям (через m2m_changed signal)
"""
logger.info(
f"[notification_signals] post_save Document id={instance.pk} "
f"created={created} sent_to_all={instance.sent_to_all}"
)
if not created:
return
document = instance
# Если документ отправляется всем - создаем уведомления сразу
if document.sent_to_all:
logger.info(
f"[notification_signals] Calling notify_all_employees for doc={document.pk}"
)
notify_all_employees(document)
else:
logger.info(
f"[notification_signals] Skipping notifications (sent_to_all=False), "
f"waiting for m2m_changed"
)
@receiver(m2m_changed, sender=Document.recipients.through)
def notify_specific_recipients(sender, instance, action, pk_set, **kwargs):
"""
Уведомляет конкретных получателей при добавлении в список.
Использует быструю обработку для большого количества получателей.
"""
logger.info(
f"[notification_signals] m2m_changed Document id={instance.pk} "
f"action={action} pk_set={pk_set} sent_to_all={instance.sent_to_all}"
)
if action != 'post_add':
logger.info(
f"[notification_signals] Skipping (action={action} != 'post_add')"
)
return
document = instance
# Не отправляем индивидуальные уведомления если документ для всех
if document.sent_to_all:
logger.info(
f"[notification_signals] Skipping (sent_to_all=True)"
)
return
recipient_count = len(pk_set or [])
logger.info(
f"[notification_signals] Processing {recipient_count} recipients"
)
# Определяем, это массовая рассылка или нет
from django.conf import settings
BULK_THRESHOLD = getattr(settings, 'NOTIFICATION_BULK_THRESHOLD', 10)
is_bulk = recipient_count >= BULK_THRESHOLD
if is_bulk:
logger.info(
f"[notification_signals] Bulk mode activated "
f"({recipient_count} >= {BULK_THRESHOLD})"
)
# Уведомляем новых получателей
created_count = 0
for user_id in pk_set:
try:
user = Employee.objects.get(id=user_id)
logger.debug(
f"[notification_signals] Creating notification for user={user_id}"
)
# Для массовой рассылки - без немедленной отправки
notification = create_document_ready_notification(
document,
user,
send_immediately=not is_bulk # False для массовой
)
# Для массовой - отправляем только веб сразу
if is_bulk and notification:
try:
settings = NotificationService.get_user_settings(
user,
notification.notification_type
)
if settings.send_web:
NotificationService.send_web_notification(notification)
notification.sent_web = True
notification.save(update_fields=['sent_web'])
except Exception as e:
logger.error(f"Error sending web notification: {e}")
created_count += 1
except Employee.DoesNotExist:
logger.warning(
f"[notification_signals] User {user_id} not found"
)
continue
logger.info(
f"[notification_signals] Created {created_count}/{recipient_count} "
f"notifications (bulk={is_bulk})"
)
# Для массовой рассылки - запускаем фоновую отправку
if is_bulk:
try:
from django.core.management import call_command
import threading
def send_pending():
try:
call_command(
'send_pending_notifications',
'--batch-size=100'
)
except Exception as e:
logger.error(f"Error in background send: {e}")
thread = threading.Thread(target=send_pending, daemon=True)
thread.start()
logger.info(
"[notification_signals] Started background email/telegram send"
)
except Exception as e:
logger.error(f"Error starting background send: {e}")
@receiver(m2m_changed, sender=Document.departments.through)
def notify_department_employees(sender, instance, action, pk_set, **kwargs):
"""
Уведомляет всех сотрудников выбранных отделов при добавлении отделов.
Уведомления получат ВСЕ текущие и будущие сотрудники этих отделов.
"""
logger.info(
f"[notification_signals] m2m_changed Document.departments "
f"id={instance.pk} action={action} pk_set={pk_set}"
)
if action != 'post_add':
logger.info(
f"[notification_signals] Skipping (action={action} != 'post_add')"
)
return
document = instance
# Не отправляем если документ для всех
if document.sent_to_all:
logger.info(
"[notification_signals] Skipping (sent_to_all=True)"
)
return
if not pk_set:
logger.warning("[notification_signals] No departments provided")
return
# Получаем всех активных сотрудников выбранных отделов
from employees.models import Department
all_employees = set()
for dept_id in pk_set:
try:
department = Department.objects.get(id=dept_id)
dept_employees = department.active_employees
logger.info(
f"[notification_signals] Department '{department.name}' "
f"has {len(dept_employees)} active employees: "
f"{[e.id for e in dept_employees]}"
)
all_employees.update(dept_employees)
logger.info(
f"[notification_signals] After update, all_employees has: "
f"{[e.id for e in all_employees]}"
)
except Department.DoesNotExist:
logger.warning(
f"[notification_signals] Department {dept_id} not found"
)
continue
# Исключаем загрузившего
if document.uploaded_by and document.uploaded_by in all_employees:
all_employees.remove(document.uploaded_by)
recipient_count = len(all_employees)
logger.info(
f"[notification_signals] Processing {recipient_count} employees "
f"from {len(pk_set)} departments"
)
# Определяем режим рассылки
from django.conf import settings
BULK_THRESHOLD = getattr(settings, 'NOTIFICATION_BULK_THRESHOLD', 10)
is_bulk = recipient_count >= BULK_THRESHOLD
if is_bulk:
logger.info(
f"[notification_signals] Bulk mode activated "
f"({recipient_count} >= {BULK_THRESHOLD})"
)
# Создаём уведомления
created_count = 0
for employee in all_employees:
try:
notification = create_document_ready_notification(
document,
employee,
send_immediately=not is_bulk
)
# Для массовой - отправляем только веб сразу
if is_bulk and notification:
try:
settings_obj = NotificationService.get_user_settings(
employee,
notification.notification_type
)
if settings_obj.send_web:
NotificationService.send_web_notification(notification)
notification.sent_web = True
notification.save(update_fields=['sent_web'])
except Exception as e:
logger.error(f"Error sending web notification: {e}")
created_count += 1
except Exception as e:
logger.error(
f"Error creating notification for user {employee.id}: {e}"
)
logger.info(
f"[notification_signals] Created {created_count}/{recipient_count} "
f"notifications (bulk={is_bulk})"
)
# Для массовой рассылки - запускаем фоновую отправку
if is_bulk:
try:
from django.core.management import call_command
import threading
def send_pending():
try:
call_command(
'send_pending_notifications',
'--batch-size=100'
)
except Exception as e:
logger.error(f"Error in background send: {e}")
thread = threading.Thread(target=send_pending, daemon=True)
thread.start()
logger.info(
"[notification_signals] Started background email/telegram send"
)
except Exception as e:
logger.error(f"Error starting background send: {e}")
@receiver(post_save, sender=DocumentAcknowledgement)
def check_all_acknowledged(sender, instance, created, **kwargs):
"""
Проверяет, все ли получатели ознакомились с документом.
Если да - уведомляет загрузившего.
"""
if not created:
return
acknowledgement = instance
document = acknowledgement.document
# Определяем общее количество получателей
if document.sent_to_all:
total_recipients = Employee.objects.filter(is_active=True).count()
else:
# Считаем получателей из recipients + departments
recipients_count = document.recipients.count()
# Добавляем сотрудников из отделов
from employees.models import Department
department_employees = set()
for department in document.departments.all():
department_employees.update(department.active_employees)
# Исключаем дубликаты с recipients
total_recipients = recipients_count + len(department_employees)
# Проверяем количество ознакомившихся
acknowledged_count = document.acknowledgements.count()
# Если все ознакомились - уведомляем загрузившего
if (acknowledged_count >= total_recipients and
total_recipients > 0 and
document.uploaded_by):
NotificationService.create_notification(
recipient=document.uploaded_by,
notification_type_code='document_signed_all',
title='Все ознакомились с документом',
message=(
f'Все сотрудники ознакомились с документом '
f'"{document.title}"'
),
content_object=document,
action_url=f'/documents/{document.id}/',
metadata={
'document_id': document.id,
'total_recipients': total_recipients,
'acknowledged_count': acknowledged_count,
}
)
# ===== Вспомогательные функции =====
def notify_all_employees(document):
"""
Отправляет уведомление о новом документе всем активным сотрудникам.
Веб-уведомления отправляются сразу, email/Telegram - асинхронно.
"""
logger.info(
f"[notification_signals] notify_all_employees doc={document.id}"
)
active_employees = Employee.objects.filter(is_active=True)
# Исключаем загрузившего документ
if document.uploaded_by:
active_employees = active_employees.exclude(
id=document.uploaded_by.id
)
count = active_employees.count()
logger.info(
f"[notification_signals] Creating {count} notifications "
f"(web immediately, email/telegram deferred)"
)
# Создаём уведомления с ЧАСТИЧНОЙ отправкой
created_count = 0
for employee in active_employees:
try:
notification = create_document_ready_notification(
document,
employee,
send_immediately=False # Не отправляем email/telegram сразу
)
# Отправляем только веб-уведомление (быстро, через WebSocket)
if notification:
try:
from notifications.services import NotificationService
settings = NotificationService.get_user_settings(
employee,
notification.notification_type
)
# Только веб
if settings.send_web:
NotificationService.send_web_notification(notification)
notification.sent_web = True
notification.save(update_fields=['sent_web'])
except Exception as e:
logger.error(f"Error sending web notification: {e}")
created_count += 1
except Exception as e:
logger.error(
f"Error creating notification for user {employee.id}: {e}"
)
logger.info(
f"[notification_signals] Created {created_count}/{count} notifications "
f"for doc={document.id}. Web sent immediately, email/telegram pending."
)
# Запускаем асинхронную отправку email/telegram
try:
from django.core.management import call_command
import threading
def send_pending():
try:
call_command('send_pending_notifications', '--batch-size=100')
except Exception as e:
logger.error(f"Error in background send: {e}")
# Запускаем в отдельном потоке
thread = threading.Thread(target=send_pending, daemon=True)
thread.start()
logger.info("[notification_signals] Started background email/telegram send")
except Exception as e:
logger.error(f"Error starting background send: {e}")
def create_document_ready_notification(document, recipient, send_immediately=True):
"""
Создает уведомление о новом документе для конкретного получателя.
Args:
document: Документ
recipient: Получатель
send_immediately: Отправить сразу или отложить (для массовых рассылок)
"""
logger.info(
f"[notification_signals] create_document_ready_notification "
f"doc={document.id} recipient={recipient.id} "
f"send_immediately={send_immediately}"
)
uploader_name = (
document.uploaded_by.get_full_name()
if document.uploaded_by
else 'Администратор'
)
notification = NotificationService.create_notification(
recipient=recipient,
notification_type_code='document_ready',
title='Новый документ на ознакомление',
message=(
f'{uploader_name} загрузил документ "{document.title}". '
f'Требуется ознакомление.'
),
content_object=document,
action_url=f'/documents/{document.id}/',
metadata={
'document_id': document.id,
'uploaded_by_id': (
document.uploaded_by.id if document.uploaded_by else None
),
'sent_to_all': document.sent_to_all,
},
send_immediately=send_immediately # Передаём параметр
)
if notification:
logger.debug(
f"[notification_signals] Created notification id={notification.id} "
f"for user={recipient.id}"
)
else:
logger.warning(
f"[notification_signals] Failed to create notification "
f"for user={recipient.id}"
)