- Split notification sending into separate WebSocket and Web Push channels - Add Web Push support for offline users - Configure VAPID keys for push authentication - Add 'aud' field to VAPID claims for WNS compatibility - Optimize subscription sync - only POST when endpoint changes - Remove debug logs and minimize server logging - Update Service Worker with improved error handling - Add Service-Worker-Allowed header for proper scope Web Push now works independently from WebSocket, enabling notifications when browser is closed (requires HTTPS in production)
485 lines
18 KiB
Python
485 lines
18 KiB
Python
"""
|
||
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_socket(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_socket(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='/documents/',
|
||
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_socket(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='/documents/',
|
||
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}"
|
||
)
|