2026-09-07 19:43:48 +03:00

455 lines
17 KiB
Python
Raw Permalink 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.

"""
Бизнес-логика отправки уведомлений для модуля Communications.
Функции:
- get_users_with_notifications_enabled - проверка настроек уведомлений
- notify_new_message - уведомление о новом сообщении (с упоминаниями и ответами)
- notify_chat_added - уведомление о добавлении в чат
ПРИМЕЧАНИЕ: Опциональная зависимость от notifications модуля.
Если notifications не установлен - уведомления просто не отправляются.
"""
from django.contrib.auth import get_user_model
from django.db.models import Q
# Опциональная зависимость от notifications (graceful degradation)
try:
from notifications.signals import notify
NOTIFICATIONS_AVAILABLE = True
except ImportError:
NOTIFICATIONS_AVAILABLE = False
notify = None
from ..mentions import get_mentionable_users, mentioned_user_ids
from ..models import ChatUserSettings
from .config import (
ActionURLs,
MessageTemplates,
NotificationVerbs,
get_chat_name,
truncate_message,
)
User = get_user_model()
def _send_notification(**kwargs):
"""
Обертка для отправки уведомлений.
Если notifications модуль не доступен - ничего не делает.
"""
if NOTIFICATIONS_AVAILABLE and notify:
notify.send(**kwargs)
# Иначе тихо игнорируем - модуль работает без уведомлений
def get_users_with_notifications_enabled(chat, users) -> dict[int, bool]:
"""
Получает словарь пользователей с их настройками уведомлений.
Оптимизированная версия - один запрос к БД вместо N.
Args:
chat: Chat объект
users: QuerySet или список пользователей
Returns:
dict: {user_id: notifications_enabled}
"""
if not users:
return {}
user_ids = [u.id for u in users]
# Один запрос для всех пользователей
settings = ChatUserSettings.objects.filter(chat=chat, user_id__in=user_ids).values(
"user_id", "notifications_enabled"
)
# Создаем словарь для быстрого доступа
settings_dict = {s["user_id"]: s["notifications_enabled"] for s in settings}
# Для пользователей без настроек - по умолчанию True
return {uid: settings_dict.get(uid, True) for uid in user_ids}
def get_chat_notification_settings(chat, users) -> dict[int, ChatUserSettings]:
"""Load the complete per-chat notification policy in one query."""
if not users:
return {}
user_ids = [user.id for user in users]
return {
item.user_id: item
for item in ChatUserSettings.objects.filter(
chat=chat,
user_id__in=user_ids,
)
}
def _notification_allowed(settings_by_user, user_id: int, verb: str) -> bool:
settings = settings_by_user.get(user_id)
return settings is None or settings.allows_notification(verb)
def notify_message_mentions(
message,
*,
recipient_ids=None,
participants_list=None,
settings_by_user=None,
) -> set[int]:
"""Send one high-priority notification to each validated mentioned user."""
chat = message.chat
author = message.author
selected_ids = mentioned_user_ids(message.mentions)
if recipient_ids is not None:
selected_ids.intersection_update({int(user_id) for user_id in recipient_ids})
selected_ids.discard(author.id)
if not selected_ids:
return set()
if participants_list is None:
participants_list = list(get_mentionable_users(chat, author=author))
if settings_by_user is None:
settings_by_user = get_chat_notification_settings(chat, participants_list)
participant_by_id = {user.id: user for user in participants_list}
recipients = [
participant_by_id[user_id]
for user_id in selected_ids
if user_id in participant_by_id
]
author_name = author.get_full_name() or author.username
notified_user_ids = set()
for recipient in recipients:
if not _notification_allowed(
settings_by_user,
recipient.id,
NotificationVerbs.MENTION,
):
continue
_send_notification(
sender=author,
recipient=recipient,
verb=NotificationVerbs.MENTION,
action_object=message,
description=truncate_message(message.content, 100),
action_url=ActionURLs.message_detail(chat.id, message.id),
data={
"title": MessageTemplates.mention(author_name),
"chat_id": chat.id,
"chat_name": get_chat_name(chat),
"chat_type": chat.type,
"can_reply": chat.can_reply,
"message_id": message.id,
"author_id": author.id,
"mentioned": True,
},
)
notified_user_ids.add(recipient.id)
return notified_user_ids
def notify_new_message(message):
"""
Отправляет уведомления о новом сообщении в чате.
Обрабатывает три типа уведомлений:
1. Упоминания (@username) - высокий приоритет
2. Ответы на сообщения - средний приоритет
3. Обычные сообщения в чате - низкий приоритет
Специальная обработка для чатов комментариев (type='comments'):
- Уведомляет автора объекта (поста/документа/заявки)
- Уведомляет других участников дискуссии
Args:
message: Экземпляр модели Message
"""
chat = message.chat
author = message.author
content = message.content
# Специальная обработка для чатов комментариев
if chat.type == "comments":
mentioned_recipient_ids = notify_message_mentions(message)
_notify_comment(message, excluded_recipient_ids=mentioned_recipient_ids)
return
# Получаем всех участников чата кроме автора
participants = get_mentionable_users(chat, author=author)
# ОПТИМИЗАЦИЯ: Загружаем настройки уведомлений ОДНИМ запросом
participants_list = list(participants)
if not participants_list:
return # Нет получателей
notification_settings = get_chat_notification_settings(chat, participants_list)
# Отслеживаем, кому уже отправили уведомления
notified_user_ids = set()
author_name = author.get_full_name() or author.username
notified_user_ids.update(
notify_message_mentions(
message,
participants_list=participants_list,
settings_by_user=notification_settings,
)
)
# 2. ОТВЕТЫ НА СООБЩЕНИЯ
if message.reply_to and message.reply_to.author_id != author.id:
original_author = message.reply_to.author
if original_author.id not in notified_user_ids:
if _notification_allowed(
notification_settings,
original_author.id,
NotificationVerbs.REPLY,
):
_send_notification(
sender=author,
recipient=original_author,
verb=NotificationVerbs.REPLY,
action_object=message,
description=truncate_message(content, 100),
action_url=ActionURLs.chat_detail(chat.id),
data={
"title": MessageTemplates.reply(author_name),
"chat_id": chat.id,
"chat_name": get_chat_name(chat),
"chat_type": chat.type,
"can_reply": chat.can_reply,
"message_id": message.id,
"reply_to_id": message.reply_to.id,
"author_id": author.id,
},
)
notified_user_ids.add(original_author.id)
# 3. ОБЫЧНЫЕ УВЕДОМЛЕНИЯ О НОВОМ СООБЩЕНИИ
if chat.type == "announcement":
notification_verb = NotificationVerbs.ANNOUNCEMENT
title = MessageTemplates.announcement(author_name)
max_length = 150
is_announcement = True
else:
notification_verb = NotificationVerbs.NEW_MESSAGE
if chat.type == "private":
title = MessageTemplates.private_message(author_name)
else:
title = MessageTemplates.group_message(author_name, get_chat_name(chat))
max_length = 100
is_announcement = False
for recipient in participants_list:
# Пропускаем тех, кому уже отправили уведомление
if recipient.id in notified_user_ids:
continue
# Проверяем настройки уведомлений
if not _notification_allowed(
notification_settings,
recipient.id,
notification_verb,
):
continue
metadata = {
"chat_id": chat.id,
"chat_name": get_chat_name(chat),
"chat_type": chat.type,
"can_reply": chat.can_reply,
"message_id": message.id,
"author_id": author.id,
}
if is_announcement:
metadata["is_announcement"] = True
_send_notification(
sender=author,
recipient=recipient,
verb=notification_verb,
action_object=message,
description=truncate_message(content, max_length),
action_url=ActionURLs.chat_detail(chat.id),
data={**metadata, "title": title},
)
def _notify_comment(message, excluded_recipient_ids=None):
"""
Отправляет уведомления о новом комментарии к объекту.
Уведомляет:
1. Автора объекта (Post/Document/Request)
2. Для Department: сотрудников отдела и role-only участников отдела
3. Автора parent comment, если это reply
Args:
message: Экземпляр модели Message (в чате type='comments')
"""
chat = message.chat
author = message.author
content = message.content
if not chat.context_object:
# Нет привязанного объекта - обычная обработка
return
recipient_ids = set()
context_obj = chat.context_object
# Определяем автора объекта в зависимости от типа
object_author = None
object_url = None
object_type = "объекта"
extra_data = {}
# Проверяем тип объекта через модель
model_name = context_obj.__class__.__name__
if model_name == "Post":
object_author = getattr(context_obj, "author", None)
object_url = f"/?post={context_obj.id}"
object_type = "публикации"
elif model_name == "Document":
object_author = getattr(context_obj, "uploaded_by", None)
object_url = f"/documents?document={context_obj.id}"
object_type = "документа"
elif model_name == "Department":
from employees.models import EmployeeDepartment, RoleAssignment
object_url = ActionURLs.chat_detail(chat.id)
object_type = "отдела"
member_ids = EmployeeDepartment.objects.filter(
department_id=context_obj.id,
is_active=True,
).values_list("employee_id", flat=True)
recipient_ids.update(member_ids)
role_only_ids = RoleAssignment.objects.filter(
role__department_id=context_obj.id,
is_active=True,
).values_list("employee_id", flat=True)
recipient_ids.update(role_only_ids)
elif model_name == "AttendanceRecord":
object_author = getattr(context_obj, "employee", None)
object_url = f"/attendance?record={context_obj.id}&comments=1"
object_type = "записи посещаемости"
admin_ids = (
User.objects.filter(
is_active=True,
)
.filter(Q(is_staff=True) | Q(is_superuser=True))
.values_list("id", flat=True)
)
recipient_ids.update(admin_ids)
extra_data = {
"attendance_record_id": context_obj.id,
"employee_id": context_obj.employee_id,
"record_date": context_obj.date.isoformat()
if getattr(context_obj, "date", None)
else None,
}
elif hasattr(context_obj, "employee"): # Request
object_author = getattr(context_obj, "employee", None)
object_url = f"/requests?request={context_obj.id}"
object_type = "заявки"
recipient_ids.update(
context_obj.recipients.filter(is_active=True).values_list("id", flat=True)
)
recipient_ids.update(
context_obj.cc_users.filter(is_active=True).values_list("id", flat=True)
)
if getattr(context_obj, "approver_id", None):
recipient_ids.add(context_obj.approver_id)
if getattr(context_obj, "sent_to_all_department", False):
from employees.models import EmployeeDepartment
dept_employee_ids = EmployeeDepartment.objects.filter(
department__in=context_obj.departments.all(),
is_active=True,
employee__is_active=True,
).values_list("employee_id", flat=True)
recipient_ids.update(dept_employee_ids)
# 1. Уведомляем автора объекта
if object_author and object_author.id != author.id:
recipient_ids.add(object_author.id)
# 2. Reply notification for comments is opt-in by explicit reply target.
if message.reply_to and message.reply_to.author_id != author.id:
recipient_ids.add(message.reply_to.author_id)
recipient_ids.discard(author.id)
recipient_ids.difference_update(excluded_recipient_ids or set())
if not recipient_ids:
return
recipients = User.objects.filter(id__in=recipient_ids, is_active=True)
# Отправляем уведомления
author_name = author.get_full_name() or author.username
for recipient in recipients:
_send_notification(
sender=author,
recipient=recipient,
verb="commented",
action_object=message,
target=context_obj,
description=truncate_message(content, 100),
action_url=object_url or f"/chat/{chat.id}/",
data={
"title": f"{author_name} оставил комментарий к {object_type}",
"chat_id": chat.id,
"message_id": message.id,
"author_id": author.id,
"object_type": model_name,
"object_id": context_obj.id,
**extra_data,
},
)
def notify_chat_added(chat, new_users, added_by=None):
"""
Отправляет уведомления пользователям о добавлении в чат.
Args:
chat: Объект Chat
new_users: Список/QuerySet новых пользователей
added_by: Пользователь, который добавил (опционально)
"""
# ОПТИМИЗАЦИЯ: Загружаем настройки одним запросом
notification_settings = get_users_with_notifications_enabled(chat, new_users)
# Создаем уведомления для новых участников
for user in new_users:
# Не отправляем уведомление тому, кто сам себя добавил
if added_by and user.id == added_by.id:
continue
# Проверяем настройки уведомлений
if not notification_settings.get(user.id, True):
continue
_send_notification(
sender=added_by,
recipient=user,
verb=NotificationVerbs.ADDED_TO_CHAT,
action_object=chat,
description=MessageTemplates.added_to_chat(get_chat_name(chat)),
action_url=ActionURLs.chat_detail(chat.id),
data={
"title": MessageTemplates.added_to_chat_title(),
"chat_id": chat.id,
"chat_name": get_chat_name(chat),
"added_by_id": added_by.id if added_by else None,
},
)