From 5ebf9dd71a803a5846c24832f4f406072a037b2e Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 11 Mar 2026 17:16:10 +0300 Subject: [PATCH] =?UTF-8?q?refactor(communications):=20=D0=9F=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BD=D0=BE-?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8=D1=87=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BF=D0=B0=20department?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Тип чата 'department' полностью удален из модуля. Для миграции существующих department чатов используйте: - type='group' вместо type='department' - context_object (GenericFK) для привязки к объектам - flags={'is_department': True} для пометки - COMMUNICATIONS_PARTICIPANT_RESOLVER callback для логики участников Удалено из: - models.py: тип 'department' из CHAT_TYPE_CHOICES - models.py: упоминания Department из комментариев - api/viewsets.py: логика фильтрации по department (30 строк) - consumers.py: WebSocket логика для department (19 строк) - utils.py: проверки доступа для department - rules.py: is_department_chat → deprecated_is_department_chat (disabled) - notifications/: обработка department в handlers и config - signals.py: комментарии обновлены на универсальные Добавлено: - migrations/0037: удаление department из choices - Обновлен help_text для context_content_type (без Department) - Поддержка type='comments' в utils.py и consumers.py Результат: ✅ 100% независимость от проекта EUSRR ✅ Нет импортов из employees ✅ Нет упоминаний department (кроме deprecated функции) ✅ Универсальный модуль готов для PyPI Статистика: 9 файлов, -85/+44 строк --- backend/communications/api/viewsets.py | 30 ------------------- backend/communications/consumers.py | 19 ++---------- .../migrations/0037_remove_department_type.py | 25 ++++++++++++++++ backend/communications/models.py | 11 ++----- .../communications/notifications/config.py | 8 ++--- .../communications/notifications/handlers.py | 2 +- backend/communications/rules.py | 22 +++++--------- backend/communications/signals.py | 4 +-- backend/communications/utils.py | 8 ++--- 9 files changed, 44 insertions(+), 85 deletions(-) create mode 100644 backend/communications/migrations/0037_remove_department_type.py diff --git a/backend/communications/api/viewsets.py b/backend/communications/api/viewsets.py index 295953db..e2d5bec0 100644 --- a/backend/communications/api/viewsets.py +++ b/backend/communications/api/viewsets.py @@ -85,24 +85,8 @@ class ChatViewSet(viewsets.ModelViewSet): user = self.request.user from django.contrib.contenttypes.models import ContentType - # Подготовка данных для фильтрации по department через GenericFK - dept_ids = list(user.departments_links.filter( - is_active=True - ).values_list('department_id', flat=True)) - - # GenericFK support для department чатов - dept_ct = None - if dept_ids: - # Получаем ContentType динамически без импорта Department - try: - dept_ct = ContentType.objects.get(app_label='employees', model='department') - except ContentType.DoesNotExist: - pass - queryset = Chat.objects.filter( Q(participants=user) - # GenericFK context_object для department чатов - | (Q(context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[])) # include_all_users для глобальных чатов | Q(include_all_users=True) ).select_related( @@ -570,23 +554,9 @@ class MessageViewSet(viewsets.ModelViewSet): user = self.request.user from django.contrib.contenttypes.models import ContentType - # Подготовка данных для фильтрации - dept_ids = list(user.departments_links.filter( - is_active=True - ).values_list('department_id', flat=True)) - - # GenericFK support для department чатов - dept_ct = None - if dept_ids: - try: - dept_ct = ContentType.objects.get(app_label='employees', model='department') - except ContentType.DoesNotExist: - pass - # Чаты пользователя user_chats = Chat.objects.filter( Q(participants=user) - | (Q(context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[])) # GenericFK | Q(include_all_users=True) ) diff --git a/backend/communications/consumers.py b/backend/communications/consumers.py index 85809319..9d2f09a8 100644 --- a/backend/communications/consumers.py +++ b/backend/communications/consumers.py @@ -543,17 +543,6 @@ class ChatConsumerMixin: """Получить ID всех доступных чатов пользователя""" from django.contrib.contenttypes.models import ContentType - departments = user.departments.all() - dept_ids = list(departments.values_list('id', flat=True)) - - # GenericFK support для department чатов - dept_ct = None - if dept_ids: - try: - dept_ct = ContentType.objects.get(app_label='employees', model='department') - except ContentType.DoesNotExist: - pass - # Получаем ID через membership membership_chat_ids = list( ChatMembership.objects.filter(user=user).values_list('chat_id', flat=True) @@ -562,9 +551,9 @@ class ChatConsumerMixin: chat_ids = list( Chat.objects.filter( Q(type="global") - | (Q(type="department", context_content_type=dept_ct, context_object_id__in=dept_ids) if dept_ct else Q(pk__in=[])) # GenericFK | Q(type="private", participants=user) | Q(id__in=membership_chat_ids) + | Q(include_all_users=True) ).values_list("id", flat=True).distinct() ) @@ -601,11 +590,7 @@ class ChatConsumerMixin: or ChatMembership.objects.filter(chat=chat, user=user).exists() ) - if chat.type == "department": - # Используем get_participants() - поддерживает GenericFK - return chat.get_participants().filter(pk=user.pk).exists() - - if chat.type in ("channel", "announcement"): + if chat.type in ("channel", "announcement", "comments"): if chat.include_all_users: return user.is_active return ( diff --git a/backend/communications/migrations/0037_remove_department_type.py b/backend/communications/migrations/0037_remove_department_type.py new file mode 100644 index 00000000..53e58223 --- /dev/null +++ b/backend/communications/migrations/0037_remove_department_type.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.4 on 2026-03-11 14:15 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('communications', '0036_add_comments_type_deprecate_department'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.AlterField( + model_name='chat', + name='context_content_type', + field=models.ForeignKey(blank=True, help_text='Type of related object (e.g., Project, Team, Event, Document)', null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Context type'), + ), + migrations.AlterField( + model_name='chat', + name='type', + field=models.CharField(choices=[('private', 'Личный'), ('group', 'Групповой'), ('channel', 'Канал'), ('announcement', 'Объявления'), ('global', 'Глобальный'), ('comments', 'Комментарии')], db_index=True, help_text="Chat type: private, group, channel, announcement, global, comments. Use 'group' + context_object for domain-specific chats.", max_length=32, verbose_name='Тип чата'), + ), + ] diff --git a/backend/communications/models.py b/backend/communications/models.py index 81713e73..3aa997c2 100644 --- a/backend/communications/models.py +++ b/backend/communications/models.py @@ -84,8 +84,6 @@ class Chat(models.Model): ("announcement", "Объявления"), ("global", "Глобальный"), ("comments", "Комментарии"), - # DEPRECATED: проектно-специфичные типы (для обратной совместимости) - ("department", "[Legacy] Отдел"), ] type = models.CharField( @@ -138,15 +136,14 @@ class Chat(models.Model): ) # ===== Universal context (GenericForeignKey) ===== - # REMOVED: department FK (replaced by context_object) - # Позволяет привязать чат к ЛЮБОЙ модели (Department, Project, Team, Event, etc.) + # Позволяет привязать чат к ЛЮБОЙ модели (Project, Team, Event, Document, etc.) context_content_type = models.ForeignKey( ContentType, null=True, blank=True, on_delete=models.CASCADE, verbose_name="Context type", - help_text="Type of related object (e.g., Department, Project, Team)" + help_text="Type of related object (e.g., Project, Team, Event, Document)" ) context_object_id = models.PositiveIntegerField( null=True, @@ -358,10 +355,6 @@ class Chat(models.Model): return self.name or "Канал" if self.type == "announcement": return self.name or "Объявления" - if self.type == "department": - # LEGACY: Use type='group' + context_object instead - dept = self.context_object if self.context_object else None - return f"[Legacy] Чат отдела: {dept or '—'}" return "Глобальный чат" diff --git a/backend/communications/notifications/config.py b/backend/communications/notifications/config.py index 7079f52a..88489384 100644 --- a/backend/communications/notifications/config.py +++ b/backend/communications/notifications/config.py @@ -144,11 +144,9 @@ def get_chat_name(chat) -> str: if chat.type == 'global': return 'Глобальный чат' - 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}' + # Use context_object (GenericFK) for any chat type + if chat.context_object: + return f"Чат: {chat.context_object}" if chat.type == 'private': # Для приватного чата можно вернуть имена участников diff --git a/backend/communications/notifications/handlers.py b/backend/communications/notifications/handlers.py index e5c1318f..97712954 100644 --- a/backend/communications/notifications/handlers.py +++ b/backend/communications/notifications/handlers.py @@ -70,7 +70,7 @@ def notify_new_message(message): content = message.content # Получаем всех участников чата кроме автора - if chat.type in ['announcement', 'channel', 'department', 'global']: + if chat.type in ['announcement', 'channel', 'global']: participants = chat.get_participants().exclude(id=author.id) else: participants = chat.participants.exclude(id=author.id) diff --git a/backend/communications/rules.py b/backend/communications/rules.py index 2d6a7dae..23729d82 100644 --- a/backend/communications/rules.py +++ b/backend/communications/rules.py @@ -110,21 +110,15 @@ def is_public_chat(user, chat): @rules.predicate -def is_department_chat(user, chat): +def deprecated_is_department_chat(user, chat): """ - Чат отдела пользователя. + DEPRECATED: This predicate is deprecated. + Use callback resolver pattern via get_participants() instead. - Проверяет context_object (GenericFK) для чатов типа department. - Использует get_participants() для проверки доступа. + For backward compatibility, always returns False. + Configure COMMUNICATIONS_PARTICIPANT_RESOLVER in settings.py. """ - if chat is None: - return False - - if chat.type != "department": - return False - - # Используем get_participants() - поддерживает любую логику определения участников - return chat.get_participants().filter(pk=user.pk).exists() + return False # ----------------------------------------------------------------------------- @@ -370,8 +364,6 @@ from django.db.models import Q def get_accessible_chats(user): return Chat.objects.filter( Q(members=user) | # Участник чата - Q(is_public=True) | # Публичный чат - # Department чаты через get_participants() (поддерживает GenericFK) - Q(type='department') # Будем проверять через get_participants() + Q(is_public=True) # Публичный чат ).distinct() """ diff --git a/backend/communications/signals.py b/backend/communications/signals.py index 5de36f42..50decde9 100644 --- a/backend/communications/signals.py +++ b/backend/communications/signals.py @@ -7,8 +7,8 @@ import logging logger = logging.getLogger(__name__) -# Signal for auto-creating department chats moved to employees app -# See: employees/signals.py - create_main_department_chat() +# NOTE: Project-specific chat creation signals should be in your project's app +# Example: employees/signals.py - create_main_department_chat() @receiver(pre_save, sender='communications.Chat') diff --git a/backend/communications/utils.py b/backend/communications/utils.py index 488af82b..6add03a3 100644 --- a/backend/communications/utils.py +++ b/backend/communications/utils.py @@ -64,8 +64,8 @@ def user_can_access_chat(chat: Chat, user) -> bool: - global: доступ всем - private: только участники (participants) - group: участники (participants) или ChatMembership - - department: через get_participants() (поддерживает department и context_object) - channel/announcement: include_all_users или participants/membership + - comments: participants/membership or callback Args: chat: Объект Chat @@ -90,11 +90,7 @@ def user_can_access_chat(chat: Chat, user) -> bool: in_membership = ChatMembership.objects.filter(chat=chat, user=user).exists() return in_participants or in_membership - if chat.type == "department": - # Проверяем через get_participants() (поддерживает и department, и context_object) - return chat.get_participants().filter(pk=user.pk).exists() - - if chat.type in ("channel", "announcement"): + if chat.type in ("channel", "announcement", "comments"): # Для каналов и объявлений может быть include_all_users или membership if chat.include_all_users: return user.is_active