Corp/backend/communications/notification_signals.py
Игорь Надеин 413c2c25b4 Исправлена отправка уведомлений для объявлений всем участникам
Проблема:
- get_participants для announcement возвращал none()
- notification_signals использовал participants вместо get_participants
- Уведомления не создавались для объявлений с include_all_employees

Решение:
- get_participants для announcement/channel теперь возвращает всех
  активных сотрудников если include_all_employees=True
- notification_signals использует get_participants для
  announcement/channel/department/global типов
- Добавлены тестовые скрипты для проверки

Тестирование:
✅ Уведомления создаются для всех участников (кроме автора)
✅ Тип уведомления: announcement_new_message
✅ Приоритет: high, каналы: web + telegram
2025-11-30 12:25:19 +03:00

245 lines
10 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 для автоматической генерации уведомлений в модуле Communications.
Обрабатывает события:
- Новое сообщение в чате
- Упоминание (@username) в сообщении
- Ответ на сообщение
- Добавление в чат
"""
import re
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 Message, Chat
from notifications.services import NotificationService
Employee = get_user_model()
@receiver(post_save, sender=Message)
def create_message_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании нового сообщения.
Обрабатывает:
1. Новое сообщение в чате (для всех участников кроме автора)
2. Упоминания (@username) в тексте
3. Ответ на сообщение (reply_to)
"""
if not created or instance.is_system or instance.is_deleted:
return
chat = instance.chat
author = instance.author
content = instance.content
# Получаем всех участников чата кроме автора
# Для announcement/channel/department используем get_participants
if chat.type in ['announcement', 'channel', 'department', 'global']:
participants = chat.get_participants.exclude(id=author.id)
else:
participants = chat.participants.exclude(id=author.id)
# 1. Обработка упоминаний (@username)
mentioned_users = extract_mentions(content)
mentioned_user_ids = []
if mentioned_users:
# Проверяем, что упомянутые пользователи есть в чате
for email in mentioned_users:
try:
user = Employee.objects.get(email=email)
if user in participants and user.id != author.id:
# Создаем уведомление об упоминании
NotificationService.create_notification(
recipient=user,
notification_type_code='chat_mention',
title=f'Вас упомянул {author.get_full_name() or author.username}',
message=truncate_message(content, 100),
content_object=instance,
action_url=f'/communications/chats/{chat.id}/?message={instance.id}',
metadata={
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'author_id': author.id,
}
)
mentioned_user_ids.append(user.id)
except Employee.DoesNotExist:
continue
# 2. Обработка ответов на сообщения
if instance.reply_to and instance.reply_to.author_id != author.id:
original_author = instance.reply_to.author
# Создаем уведомление об ответе (если автор не упомянут явно)
if original_author.id not in mentioned_user_ids:
NotificationService.create_notification(
recipient=original_author,
notification_type_code='chat_reply',
title=f'{author.get_full_name() or author.username} ответил на ваше сообщение',
message=truncate_message(content, 100),
content_object=instance,
action_url=f'/communications/chats/{chat.id}/?message={instance.id}',
metadata={
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'reply_to_id': instance.reply_to.id,
'author_id': author.id,
}
)
# 3. Обычное уведомление о новом сообщении
# Отправляем только тем, кто НЕ получил упоминание или ответ
excluded_ids = set(mentioned_user_ids)
if instance.reply_to and instance.reply_to.author_id != author.id:
excluded_ids.add(instance.reply_to.author_id)
# Определяем, кому отправлять обычные уведомления
# Для приватных чатов - всегда отправляем
# Для announcement - отправляем ВСЕМ участникам (это важные объявления)
# Для групповых - можно добавить логику (например, только онлайн или с настройками)
if chat.type == 'private':
recipients_for_new_message = participants.exclude(id__in=excluded_ids)
for recipient in recipients_for_new_message:
NotificationService.create_notification(
recipient=recipient,
notification_type_code='chat_new_message',
title=f'Новое сообщение от {author.get_full_name() or author.username}',
message=truncate_message(content, 100),
content_object=instance,
action_url=f'/communications/chats/{chat.id}/',
metadata={
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'author_id': author.id,
}
)
elif chat.type == 'announcement':
# Для объявлений отправляем ВСЕМ участникам
recipients_for_announcement = participants.exclude(id__in=excluded_ids)
for recipient in recipients_for_announcement:
NotificationService.create_notification(
recipient=recipient,
notification_type_code='announcement_new_message',
title=f'Новое объявление от {author.get_full_name() or author.username}',
message=truncate_message(content, 150),
content_object=instance,
action_url=f'/communications/chats/{chat.id}/',
metadata={
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'author_id': author.id,
'is_announcement': True,
}
)
elif chat.type in ['group', 'department', 'channel']:
# Для групповых чатов - отправляем уведомление только если есть упоминание или ответ
# Обычные сообщения не создают уведомления, чтобы не спамить
pass
@receiver(m2m_changed, sender=Chat.participants.through)
def create_chat_added_notifications(sender, instance, action, pk_set, **kwargs):
"""
Создает уведомления когда пользователя добавляют в чат.
"""
if action != 'post_add':
return
chat = instance
# Получаем информацию о том, кто добавил (если доступно)
# Это может быть последний изменивший или создатель чата
added_by = getattr(chat, 'created_by', None) or chat.participants.first()
# Создаем уведомления для новых участников
for user_id in pk_set:
try:
user = Employee.objects.get(id=user_id)
# Не отправляем уведомление тому, кто сам себя добавил
if added_by and user.id == added_by.id:
continue
NotificationService.create_notification(
recipient=user,
notification_type_code='chat_added_to_chat',
title=f'Вас добавили в чат',
message=f'Вы были добавлены в чат "{get_chat_name(chat)}"',
content_object=chat,
action_url=f'/communications/chats/{chat.id}/',
metadata={
'chat_id': chat.id,
'chat_type': chat.type,
'chat_name': get_chat_name(chat),
'added_by_id': added_by.id if added_by else None,
}
)
except Employee.DoesNotExist:
continue
# ===== Вспомогательные функции =====
def extract_mentions(text):
"""
Извлекает email'ы из упоминаний вида @email.
Returns:
list: Список email'ов без символа @
"""
if not text:
return []
# Паттерн: @ + email формат
pattern = r'@([\w.+-]+@[\w.-]+\.[\w]+)'
mentions = re.findall(pattern, text)
return list(set(mentions)) # Уникальные значения
def truncate_message(text, max_length=100):
"""
Обрезает текст сообщения до указанной длины.
"""
if not text:
return ''
if len(text) <= max_length:
return text
return text[:max_length - 3] + '...'
def get_chat_name(chat):
"""
Возвращает название чата в зависимости от типа.
"""
if chat.name:
return chat.name
if chat.type == 'global':
return 'Глобальный чат'
if chat.type == 'department' and chat.department:
return f'Чат отдела: {chat.department.name}'
if chat.type == 'private':
# Для приватного чата можно вернуть имена участников
participants = chat.participants.all()[:2]
if participants:
names = [p.get_full_name() or p.username for p in participants]
return ', '.join(names)
return 'Чат'