From f56e256a2aa12df75ce2f446d47e0576ce9bd08b Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 11 Mar 2026 14:30:23 +0300 Subject: [PATCH] feat(communications): Phase 2 - Update views, signals, rules for GenericFK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/communications/admin.py | 10 ++++-- backend/communications/apps.py | 22 ++++++++++-- backend/communications/models.py | 4 ++- .../communications/notifications/config.py | 7 ++-- backend/communications/rules.py | 19 ++++++++-- backend/communications/signals.py | 34 +++++++++++++++--- backend/communications/views.py | 35 +++++++++++++------ 7 files changed, 107 insertions(+), 24 deletions(-) diff --git a/backend/communications/admin.py b/backend/communications/admin.py index 6a59f071..4c92acaf 100644 --- a/backend/communications/admin.py +++ b/backend/communications/admin.py @@ -42,6 +42,9 @@ class ChatAdmin(admin.ModelAdmin): "created_at" ) list_filter = ("type", "is_main", "created_at") + # Поддержка поиска по старому и новому полю + # department__name - старое поле (DEPRECATED) + # Для context_object поиск невозможен через GenericFK search_fields = ("name", "description", "department__name") ordering = ("-created_at",) filter_horizontal = ("participants",) @@ -49,8 +52,11 @@ class ChatAdmin(admin.ModelAdmin): readonly_fields = ("created_at",) def get_readonly_fields(self, request, obj=None): - if obj and obj.is_main: - return self.readonly_fields + ("type", "department", "is_main") + # Главные чаты (is_main=True или flags['is_primary']=True) нельзя редактировать + if obj: + is_primary = obj.is_main or (obj.flags and obj.flags.get('is_primary')) + if is_primary: + return self.readonly_fields + ("type", "department", "is_main", "flags", "context_object_id", "context_content_type") return self.readonly_fields diff --git a/backend/communications/apps.py b/backend/communications/apps.py index 72f5775a..054616ae 100644 --- a/backend/communications/apps.py +++ b/backend/communications/apps.py @@ -12,8 +12,26 @@ class CommunicationsConfig(AppConfig): import communications.rules # django-rules: регистрация предикатов и правил доступа def create_main_global_chat(sender, **kwargs): + """ + Создает главный глобальный чат при первом запуске. + + Использует flags['is_primary'] вместо is_main. + Для обратной совместимости также устанавливает is_main=True. + """ from communications.models import Chat + from django.db.models import Q - if not Chat.objects.filter(type="global", is_main=True).exists(): - Chat.objects.create(type="global", is_main=True) + # Проверяем по обоим полям (старому и новому) + if not Chat.objects.filter( + Q(type="global") & ( + Q(is_main=True) | Q(flags__is_primary=True) + ) + ).exists(): + Chat.objects.create( + type="global", + # NEW: flags + flags={'is_primary': True}, + # DEPRECATED: для обратной совместимости + is_main=True + ) diff --git a/backend/communications/models.py b/backend/communications/models.py index 8e4050df..269362c3 100644 --- a/backend/communications/models.py +++ b/backend/communications/models.py @@ -366,7 +366,9 @@ class Chat(models.Model): if self.type == "group": return self.name or "Групповой чат" if self.type == "department": - return f"Чат отдела: {self.department or '—'}" + # Проверяем context_object (GenericFK) или старое поле department + dept = self.context_object if self.context_object else self.department + return f"Чат отдела: {dept or '—'}" if self.type == "channel": return self.name or "Канал" if self.type == "announcement": diff --git a/backend/communications/notifications/config.py b/backend/communications/notifications/config.py index a1a07cc9..7079f52a 100644 --- a/backend/communications/notifications/config.py +++ b/backend/communications/notifications/config.py @@ -144,8 +144,11 @@ def get_chat_name(chat) -> str: if chat.type == 'global': return 'Глобальный чат' - if chat.type == 'department' and chat.department: - return f'Чат отдела: {chat.department.name}' + if chat.type == 'department': + # Проверяем context_object (GenericFK) или старое поле department + dept = chat.context_object if hasattr(chat, 'context_object') and chat.context_object else getattr(chat, 'department', None) + if dept: + return f'Чат отдела: {dept.name}' if chat.type == 'private': # Для приватного чата можно вернуть имена участников diff --git a/backend/communications/rules.py b/backend/communications/rules.py index 05fd5c0f..8ae5b699 100644 --- a/backend/communications/rules.py +++ b/backend/communications/rules.py @@ -111,12 +111,25 @@ def is_public_chat(user, chat): @rules.predicate def is_department_chat(user, chat): - """Чат отдела пользователя""" + """ + Чат отдела пользователя. + + Проверяет как старое поле department (DEPRECATED), + так и новое context_object (GenericFK). + """ if chat is None or not hasattr(user, 'department'): return False - if hasattr(chat, 'department'): - return chat.department == user.department + # 1. Проверяем старое поле department (для обратной совместимости) + if hasattr(chat, 'department') and chat.department: + if chat.department == user.department: + return True + + # 2. Проверяем новое поле context_object (GenericFK) + if hasattr(chat, 'context_object') and chat.context_object: + from employees.models import Department + if isinstance(chat.context_object, Department): + return chat.context_object == user.department return False diff --git a/backend/communications/signals.py b/backend/communications/signals.py index bad45845..f0d947c2 100644 --- a/backend/communications/signals.py +++ b/backend/communications/signals.py @@ -1,6 +1,7 @@ # backend\communications\signals.py +from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile -from django.db.models import F +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 @@ -11,16 +12,41 @@ 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: - if not Chat.objects.filter( - type="department", department=instance, is_main=True - ).exists(): + 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}" ) diff --git a/backend/communications/views.py b/backend/communications/views.py index 69a0a901..27d9e4bf 100644 --- a/backend/communications/views.py +++ b/backend/communications/views.py @@ -76,14 +76,15 @@ def user_can_access_chat(chat: Chat, user) -> bool: print(f"[CHAT DEBUG] Group chat, participants={in_participants}, membership={in_membership}, result={result}") return result - if chat.type == "department" and chat.department_id: - result = chat.get_participants.filter(pk=user.pk).exists() + if chat.type == "department": + # Проверяем через get_participants() (поддерживает и department, и context_object) + result = chat.get_participants().filter(pk=user.pk).exists() print(f"[CHAT DEBUG] Department chat, result={result}") return result if chat.type in ("channel", "announcement"): - # Для каналов и объявлений может быть include_all или membership - if chat.include_all_employees: + # Для каналов и объявлений может быть include_all_users или membership + if chat.include_all_users: return user.is_active # Проверяем и participants и membership для гибкости result = ( @@ -136,14 +137,21 @@ class ChatListView(LoginRequiredMixin, ListView): # Получаем ID чатов, где пользователь является участником # через membership from communications.models import ChatMembership + from django.contrib.contenttypes.models import ContentType + membership_chat_ids = ChatMembership.objects.filter( user=user ).values_list('chat_id', flat=True) + + # Для department: проверяем и старое поле department, и новое context_object + dept_ct = ContentType.objects.get_for_model(departments.model) + dept_ids = list(departments.values_list('id', flat=True)) qs = ( Chat.objects.filter( Q(type="global") - | Q(type="department", department__in=departments) + | Q(type="department", department__in=departments) # старое поле + | Q(type="department", context_content_type=dept_ct, context_object_id__in=dept_ids) # новое поле | Q(type="private", participants=user) | Q(id__in=membership_chat_ids) # group/channel/announcement ) @@ -229,6 +237,8 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView): # Получаем ID чатов через membership from communications.models import ChatMembership + from django.contrib.contenttypes.models import ContentType + membership_chat_ids = list( ChatMembership.objects.filter(user=user).values_list('chat_id', flat=True) ) @@ -241,10 +251,14 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView): f"User departments: {list(departments.values_list('id', flat=True))}, " f"membership_chat_ids: {membership_chat_ids}" ) + + # Для department: проверяем и старое поле, и новое + dept_ct = ContentType.objects.get_for_model(departments.model) qs = Chat.objects.filter( Q(type="global") - | Q(type="department", department__in=departments) + | Q(type="department", department__in=departments) # старое поле + | Q(type="department", context_content_type=dept_ct, context_object_id__in=dept_ids) # новое поле | Q(type="private", participants=user) | Q(id__in=membership_chat_ids) ).distinct() @@ -470,11 +484,12 @@ def chat_mark_read(request, pk: int): return True if c.type in ("private", "group"): # Групповые и личные чаты return c.participants.filter(pk=u.pk).exists() - if c.type == "department" and c.department_id: - return c.get_participants.filter(pk=u.pk).exists() + if c.type == "department": + # Используем get_participants() - поддерживает и department, и context_object + return c.get_participants().filter(pk=u.pk).exists() if c.type in ("channel", "announcement"): - # Для каналов и объявлений проверяем participants или include_all - if c.include_all_employees: + # Для каналов и объявлений проверяем participants или include_all_users + if c.include_all_users: return u.is_active return c.participants.filter(pk=u.pk).exists() return False