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

252 lines
9.1 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.

# backend\communications\signals.py
import logging
from django.conf import settings
from django.core.files.base import ContentFile
from django.db.models import F
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.utils.module_loading import import_string
logger = logging.getLogger(__name__)
@receiver(post_save, sender="communications.ChatMembership")
def synchronize_chat_membership_realtime(sender, instance, **kwargs):
"""Keep live consumer group membership aligned with database access."""
from communications.realtime import schedule_chat_access_changed
schedule_chat_access_changed(
chat_id=instance.chat_id,
user_id=instance.user_id,
membership_active=instance.is_active,
)
@receiver(post_delete, sender="communications.ChatMembership")
def revoke_deleted_chat_membership_realtime(sender, instance, **kwargs):
from communications.realtime import schedule_chat_access_changed
schedule_chat_access_changed(
chat_id=instance.chat_id,
user_id=instance.user_id,
membership_active=False,
)
# NOTE: Project-specific chat creation signals should be in your project's app
# Example: employees/signals.py - create_main_department_chat()
def _get_image_compressor():
"""
Получает функцию сжатия изображений из настроек или использует дефолтную.
Settings:
COMMUNICATIONS_IMAGE_COMPRESSOR (str): Путь к функции сжатия.
По умолчанию: 'common.image_utils.compress_avatar'
(проектно-специфичный)
Для standalone: None (отключает сжатие)
Пример: 'myapp.utils.compress_image'
Returns:
callable or None: Функция сжатия или None если отключено
"""
compressor_path = getattr(
settings,
"COMMUNICATIONS_IMAGE_COMPRESSOR",
"common.image_utils.compress_avatar", # backward compatibility
)
if compressor_path is None:
return None
try:
return import_string(compressor_path)
except (ImportError, AttributeError) as e:
logger.warning(
"Chat avatar compressor is unavailable",
extra={
"event": "chat.avatar.compressor_unavailable",
"compressor_configured": bool(compressor_path),
"error_type": type(e).__name__,
},
)
return None
@receiver(pre_save, sender="communications.Chat")
def compress_and_cleanup_chat_avatar(sender, instance, **kwargs):
"""
Сжимает новый аватар чата и удаляет старый файл.
Использует COMMUNICATIONS_IMAGE_COMPRESSOR из settings.
Если compressor не настроен - пропускает сжатие.
"""
if not instance.avatar:
return
compress_avatar = _get_image_compressor()
# Если это новый объект (нет pk), просто сжимаем
if not instance.pk:
if compress_avatar:
try:
compressed = compress_avatar(instance.avatar.read())
if compressed:
instance.avatar.save(
instance.avatar.name,
ContentFile(compressed),
save=False,
)
except Exception as e:
logger.exception(
"New chat avatar compression failed",
extra={
"event": "chat.avatar.compression_failed",
"chat_id": instance.pk,
"error_type": type(e).__name__,
},
)
return
# Проверяем, изменился ли файл аватара
try:
old_instance = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
return
old_avatar = old_instance.avatar
new_avatar = instance.avatar
# Если путь изменился - это новый файл
if old_avatar and new_avatar and old_avatar.name != new_avatar.name:
try:
# Сжимаем новый аватар (если compressor настроен)
if compress_avatar:
compressed = compress_avatar(new_avatar.read())
if compressed:
new_avatar.save(
new_avatar.name, ContentFile(compressed), save=False
)
# Удаляем старый файл
if old_avatar.name:
old_avatar.delete(save=False)
logger.info(
"Old chat avatar deleted",
extra={
"event": "chat.avatar.old_file_deleted",
"chat_id": instance.pk,
},
)
except Exception as e:
logger.exception(
"Chat avatar processing failed",
extra={
"event": "chat.avatar.processing_failed",
"chat_id": instance.pk,
"error_type": type(e).__name__,
},
)
@receiver(post_save, sender="communications.Message")
def increment_unread_count_on_new_message(sender, instance, created, **kwargs):
"""
Инкрементирует unread_count для всех участников чата (кроме автора)
при создании нового сообщения.
Денормализация: вместо подсчета COUNT(*) в реальном времени,
обновляем кешированный счетчик инкрементом.
"""
if not created or instance.is_deleted:
return
from communications.mentions import mentioned_user_ids
from communications.models import ChatReadState
try:
# Получаем всех участников чата
chat = instance.chat
participants = chat.get_participants().exclude(id=instance.author_id)
mentioned_ids = mentioned_user_ids(instance.mentions)
# Инкрементируем счетчик для существующих ChatReadState
# Только для тех, кто еще не прочитал это сообщение
updated_count = (
ChatReadState.objects.filter(chat=chat, user__in=participants)
.exclude(
# Пропускаем тех, кто уже прочитал дальше этого сообщения
last_read_message_id__gte=instance.id
)
.update(unread_count=F("unread_count") + 1)
)
updated_mention_count = 0
if mentioned_ids:
updated_mention_count = (
ChatReadState.objects.filter(
chat=chat,
user__in=participants,
user_id__in=mentioned_ids,
)
.exclude(last_read_message_id__gte=instance.id)
.update(unread_mention_count=F("unread_mention_count") + 1)
)
# Создаем ChatReadState для участников, у которых его еще нет
existing_users = ChatReadState.objects.filter(
chat=chat, user__in=participants
).values_list("user_id", flat=True)
new_users = list(participants.exclude(id__in=existing_users))
if new_users:
# Создаем ChatReadState с unread_count=1 для новых участников
ChatReadState.objects.bulk_create(
[
ChatReadState(
chat=chat,
user=user,
last_read_message=None,
unread_count=1,
unread_mention_count=1 if user.id in mentioned_ids else 0,
)
for user in new_users
],
ignore_conflicts=True,
)
logger.info(
"Chat read states created for new message",
extra={
"event": "chat.read_states.created",
"chat_id": chat.id,
"message_id": instance.id,
"state_count": len(new_users),
},
)
logger.debug(
"Chat unread counters updated",
extra={
"event": "chat.unread_counters.updated",
"chat_id": chat.id,
"message_id": instance.id,
"state_count": updated_count,
"mention_state_count": updated_mention_count,
},
)
except Exception as e:
logger.exception(
"Chat unread counter update failed",
extra={
"event": "chat.unread_counters.update_failed",
"message_id": instance.id,
"chat_id": instance.chat_id,
"error_type": type(e).__name__,
},
)