lizerk f56e256a2a feat(communications): Phase 2 - Update views, signals, rules for GenericFK
BREAKING CHANGES (backward compatible):
- All code updated to support both old and new fields
- context_object (GenericFK) and flags now primary
- department and is_main still work (DEPRECATED)

FILES UPDATED:

1. signals.py:
   - create_main_department_chat() uses context_object + flags
   - Creates both old and new fields for compatibility
   - Checks existence using Q() for both fields

2. views.py (4 critical updates):
   - has_user_access_to_chat(): uses get_participants() (supports both)
   - ChatListView.get_queryset(): checks department OR context_object
   - ChatDetailView.get_queryset(): checks department OR context_object
   - mark_as_read_ajax(): uses get_participants() and include_all_users

3. rules.py:
   - is_department_chat(): checks both department and context_object
   - Supports GenericFK for Department context

4. apps.py:
   - create_main_global_chat(): checks and sets flags['is_primary']
   - Backward compatible with is_main

5. admin.py:
   - get_readonly_fields(): checks flags['is_primary'] OR is_main
   - Prevents editing primary chats

6. notifications/config.py:
   - get_chat_display_name(): uses context_object OR department

7. models.py:
   - Chat.__str__(): uses context_object OR department

TESTED:
✅ Signal creates chat with context_object + flags
✅ get_participants() works with both fields
✅ __str__ displays correctly
✅ is_department_chat rule works
✅ Backward compatibility preserved
2026-03-11 14:30:23 +03:00

170 lines
6.5 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.

# backend\communications\signals.py
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.db.models import F, Q
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from employees.models import Department
import logging
logger = logging.getLogger(__name__)
@receiver(post_save, sender=Department)
def create_main_department_chat(sender, instance, created, **kwargs):
"""
Создает главный чат для нового отдела.
Использует context_object (GenericFK) и flags['is_primary'].
Для обратной совместимости также заполняет department и is_main (DEPRECATED).
"""
from communications.models import Chat
if created:
dept_ct = ContentType.objects.get_for_model(Department)
# Проверка: существует ли уже чат для этого отдела
# (проверяем и старые, и новые поля)
existing = Chat.objects.filter(
Q(type="department") & (
Q(department=instance) |
Q(context_content_type=dept_ct, context_object_id=instance.id)
) & (
Q(is_main=True) | Q(flags__is_primary=True)
)
)
if not existing.exists():
# Создаем чат с ОБОИМИ способами привязки (для совместимости)
Chat.objects.create(
type="department",
# NEW: GenericFK
context_content_type=dept_ct,
context_object_id=instance.id,
# NEW: flags
flags={'is_primary': True},
# DEPRECATED: старые поля (для обратной совместимости)
department=instance,
is_main=True,
# Имя чата
name=f"Основной чат {instance.name}"
)
@receiver(pre_save, sender='communications.Chat')
def compress_and_cleanup_chat_avatar(sender, instance, **kwargs):
"""
Сжимает новый аватар чата и удаляет старый файл
"""
if not instance.avatar:
return
# Если это новый объект (нет pk), просто сжимаем
if not instance.pk:
try:
from common.image_utils import compress_avatar
compressed = compress_avatar(instance.avatar.read())
if compressed:
instance.avatar.save(
instance.avatar.name,
ContentFile(compressed),
save=False
)
except Exception as e:
logger.error(f"Error compressing new chat avatar: {e}")
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:
# Сжимаем новый аватар
from common.image_utils import 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(
f"Deleted old chat avatar: {old_avatar.name}"
)
except Exception as e:
logger.error(f"Error processing chat avatar: {e}")
@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.models import ChatReadState
try:
# Получаем всех участников чата
chat = instance.chat
participants = chat.get_participants.exclude(id=instance.author_id)
# Инкрементируем счетчик для существующих 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
)
# Создаем ChatReadState для участников, у которых его еще нет
existing_users = ChatReadState.objects.filter(
chat=chat,
user__in=participants
).values_list('user_id', flat=True)
new_users = participants.exclude(id__in=existing_users)
if new_users.exists():
# Создаем ChatReadState с unread_count=1 для новых участников
ChatReadState.objects.bulk_create([
ChatReadState(
chat=chat,
user=user,
last_read_message=None,
unread_count=1
)
for user in new_users
], ignore_conflicts=True)
logger.info(
f"[increment_unread] Created {new_users.count()} new ChatReadState for msg={instance.id}"
)
logger.debug(
f"[increment_unread] Updated {updated_count} ChatReadState for msg={instance.id} in chat={chat.id}"
)
except Exception as e:
logger.error(f"Error incrementing unread_count for message {instance.id}: {e}", exc_info=True)