455 lines
17 KiB
Python
455 lines
17 KiB
Python
"""
|
||
Бизнес-логика отправки уведомлений для модуля 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,
|
||
},
|
||
)
|