From 6ddd3f69baf8b6d0a8c813d5c760943fde722907 Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 11 Mar 2026 17:06:06 +0300 Subject: [PATCH] =?UTF-8?q?feat(communications):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=82=D0=B8=D0=BF=20'comments?= =?UTF-8?q?',=20=D0=BF=D0=BE=D0=BC=D0=B5=D1=82=D0=B8=D1=82=D1=8C=20'depart?= =?UTF-8?q?ment'=20=D0=BA=D0=B0=D0=BA=20legacy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Тип 'department' помечен как deprecated. Изменения: - Добавлен новый тип чата 'comments' для систем комментариев - Тип 'department' перемещен в конец списка и помечен как [Legacy] - Обновлен help_text: рекомендуется использовать 'group' + context_object - Обновлен __str__() для обработки типа 'comments' - Создана миграция 0036_add_comments_type_deprecate_department Универсальность: - 'comments' + context_object работает с любыми моделями - Треды, реакции, вложения доступны из коробки - Real-time обновления через WebSocket - Автоматические уведомления при упоминаниях и ответах Рекомендации: - Для проектно-специфичных чатов используй type='group' + context_object - Department: type='group' + context_object=department + flags={'is_department': True} - Comments: type='comments' + context_object=any_model Документация: - Создан docs/guides/COMMENTS_USAGE.md с примерами - Описаны все возможности системы комментариев - Примеры API, WebSocket, миграции с legacy типов --- ..._add_comments_type_deprecate_department.py | 18 + backend/communications/models.py | 18 +- backend/docs/guides/COMMENTS_USAGE.md | 335 ++++++++++++++++++ 3 files changed, 365 insertions(+), 6 deletions(-) create mode 100644 backend/communications/migrations/0036_add_comments_type_deprecate_department.py create mode 100644 backend/docs/guides/COMMENTS_USAGE.md diff --git a/backend/communications/migrations/0036_add_comments_type_deprecate_department.py b/backend/communications/migrations/0036_add_comments_type_deprecate_department.py new file mode 100644 index 00000000..88a8ed90 --- /dev/null +++ b/backend/communications/migrations/0036_add_comments_type_deprecate_department.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2026-03-11 14:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('communications', '0035_remove_department_field'), + ] + + operations = [ + migrations.AlterField( + model_name='chat', + name='type', + field=models.CharField(choices=[('private', 'Личный'), ('group', 'Групповой'), ('channel', 'Канал'), ('announcement', 'Объявления'), ('global', 'Глобальный'), ('comments', 'Комментарии'), ('department', '[Legacy] Отдел')], 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 3a993136..81713e73 100644 --- a/backend/communications/models.py +++ b/backend/communications/models.py @@ -80,10 +80,12 @@ class Chat(models.Model): CHAT_TYPE_CHOICES = [ ("private", "Личный"), ("group", "Групповой"), - ("department", "Отдел"), ("channel", "Канал"), ("announcement", "Объявления"), ("global", "Глобальный"), + ("comments", "Комментарии"), + # DEPRECATED: проектно-специфичные типы (для обратной совместимости) + ("department", "[Legacy] Отдел"), ] type = models.CharField( @@ -91,7 +93,7 @@ class Chat(models.Model): choices=CHAT_TYPE_CHOICES, verbose_name="Тип чата", db_index=True, - help_text="Chat type: private, group, channel, announcement, department, global" + help_text="Chat type: private, group, channel, announcement, global, comments. Use 'group' + context_object for domain-specific chats." ) # Новые базовые поля @@ -348,14 +350,18 @@ class Chat(models.Model): return f"Личный чат: {names}" if self.type == "group": return self.name or "Групповой чат" - if self.type == "department": - # Used context_object (GenericFK) - dept = self.context_object if self.context_object else None - return f"Чат отдела: {dept or '—'}" + if self.type == "comments": + # Comments linked to any object via GenericFK + context = self.context_object if self.context_object else None + return f"Комментарии: {context or self.name or '—'}" if self.type == "channel": 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/docs/guides/COMMENTS_USAGE.md b/backend/docs/guides/COMMENTS_USAGE.md new file mode 100644 index 00000000..3f1c07ae --- /dev/null +++ b/backend/docs/guides/COMMENTS_USAGE.md @@ -0,0 +1,335 @@ +# Использование Communications для комментариев + +## Новый тип чата: `comments` + +С версии migrations/0036 добавлен специальный тип чата **"comments"** для систем комментариев. + +## Быстрый старт + +### 1. Создание чата комментариев + +```python +from django.contrib.contenttypes.models import ContentType +from communications.models import Chat, Message + +# Привязать комментарии к любому объекту +document = Document.objects.get(id=123) + +comments_chat = Chat.objects.create( + type='comments', + name=f'Комментарии: {document.title}', + context_object=document, # GenericFK - работает с ЛЮБОЙ моделью + created_by=request.user, + flags={'allow_replies': True, 'allow_reactions': True} +) +``` + +### 2. Добавление комментария + +```python +# Простой комментарий +comment = Message.objects.create( + chat=comments_chat, + author=request.user, + content="Отличный документ!" +) + +# Комментарий с вложениями +comment = Message.objects.create( + chat=comments_chat, + author=request.user, + content="См. прикрепленный файл", + has_attachments=True +) +MessageAttachment.objects.create( + message=comment, + file=uploaded_file, + file_type='file', + file_name='report.pdf', + file_size=12345 +) +``` + +### 3. Вложенные комментарии (треды) + +```python +# Ответ на комментарий +reply = Message.objects.create( + chat=comments_chat, + author=another_user, + content="Согласен!", + reply_to=comment, # Прямой ответ + thread_root=comment # Корень треда +) + +# Получить все ответы на комментарий +replies = comment.direct_replies.filter(is_deleted=False) + +# Получить весь тред +thread = Message.objects.filter( + thread_root=comment, + is_deleted=False +).order_by('created_at') +``` + +### 4. Редактирование и удаление + +```python +# Редактировать комментарий +from django.utils import timezone + +comment.content = "Отличный документ! (обновлено)" +comment.is_edited = True +comment.edited_at = timezone.now() +comment.save() + +# Мягкое удаление +comment.is_deleted = True +comment.deleted_at = timezone.now() +comment.deleted_by = request.user +comment.save() +``` + +### 5. Реакции (эмодзи) + +```python +from communications.models import MessageReaction + +# Добавить реакцию +MessageReaction.objects.create( + message=comment, + user=request.user, + emoji='👍' +) + +# Получить сводку по реакциям +reactions_summary = comment.get_reactions_summary() +# { +# '👍': {'count': 3, 'users': [1, 2, 3], 'user_names': ['User1', ...]}, +# '❤️': {'count': 1, 'users': [4], 'user_names': ['User4']} +# } +``` + +### 6. Уведомления (автоматические) + +Модуль автоматически отправляет уведомления при: +- **Упоминаниях**: `@username` в тексте комментария +- **Ответах**: reply_to на чужой комментарий +- **Новых комментариях**: всем участникам чата (кроме автора) + +```python +# Упомянуть пользователя +comment = Message.objects.create( + chat=comments_chat, + author=request.user, + content="@john.doe посмотри этот документ" +) +# → john.doe получит уведомление автоматически +``` + +### 7. Real-time обновления через WebSocket + +```javascript +// Frontend: подключение к WebSocket +const ws = new WebSocket('ws://localhost:8000/ws/realtime/'); + +// Подписаться на комментарии +ws.send(JSON.stringify({ + type: 'chat.subscribe', + chat_id: commentsChat.id +})); + +// Получать обновления в реальном времени +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.type === 'chat.message') { + // Новый комментарий → обновить UI + addCommentToUI(data.message); + } +}; +``` + +## API эндпоинты + +### Получить список комментариев + +```http +GET /api/v1/communications/chats/{chat_id}/messages/?page=1&page_size=20 + +Response: +{ + "count": 42, + "next": "...", + "results": [ + { + "id": 123, + "author": {...}, + "content": "Отличный документ!", + "created_at": "2026-03-11T15:30:00Z", + "is_edited": false, + "reply_to": null, + "thread_root": null, + "reactions_summary": { + "👍": {"count": 3, "users": [1,2,3]} + }, + "attachments": [...] + } + ] +} +``` + +### Создать комментарий + +```http +POST /api/v1/communications/chats/{chat_id}/messages/ + +{ + "content": "Отличный документ!", + "reply_to": 123 // опционально +} +``` + +### Редактировать комментарий + +```http +PATCH /api/v1/communications/messages/{message_id}/ + +{ + "content": "Отличный документ! (обновлено)" +} +``` + +### Удалить комментарий + +```http +DELETE /api/v1/communications/messages/{message_id}/ +``` + +### Добавить реакцию + +```http +POST /api/v1/communications/messages/{message_id}/react/ + +{ + "emoji": "👍" +} +``` + +## Преимущества использования Communications для комментариев + +✅ **Треды** - вложенные комментарии "из коробки" +✅ **Реакции** - эмодзи-реакции на комментарии +✅ **Вложения** - картинки, файлы, видео +✅ **Редактирование** - с историей изменений +✅ **Упоминания** - @username с автоматическими уведомлениями +✅ **Real-time** - WebSocket обновления +✅ **Универсальность** - GenericFK работает с любыми моделями +✅ **Оптимизация** - денормализованные счетчики, prefetch + +## Миграция с "department" на универсальные типы + +### ❌ Старый подход (проектно-специфичный) + +```python +# Чат отдела (жестко привязан к EUSRR) +chat = Chat.objects.create( + type='department', # DEPRECATED! + department=department_instance +) +``` + +### ✅ Новый подход (универсальный) + +```python +# Используй type='group' + context_object +chat = Chat.objects.create( + type='group', # Универсальный тип + name=f'Чат отдела: {department.name}', + context_object=department, # GenericFK + flags={'is_department_chat': True} +) + +# Или для комментариев к объектам отдела +chat = Chat.objects.create( + type='comments', + name=f'Обсуждение: {department.name}', + context_object=department +) +``` + +## Настройка участников через callback + +Для проектно-специфичной логики участников используй callback: + +```python +# settings.py +COMMUNICATIONS_PARTICIPANT_RESOLVER = 'myproject.utils.get_chat_participants' + +# myproject/utils.py +def get_chat_participants(chat): + """ + Кастомная логика определения участников. + + Возвращает QuerySet пользователей или None для fallback. + """ + if chat.type == 'comments': + # Комментарии видят только те, кто имеет доступ к объекту + if hasattr(chat.context_object, 'get_allowed_users'): + return chat.context_object.get_allowed_users() + + # Для department используй GenericFK + if chat.flags.get('is_department_chat'): + from employees.models import EmployeeDepartment + dept = chat.context_object + user_ids = EmployeeDepartment.objects.filter( + department=dept, + is_active=True + ).values_list('employee_id', flat=True) + return User.objects.filter(id__in=user_ids) + + return None # Fallback к стандартной логике +``` + +## Примеры использования + +### Комментарии к документу + +```python +document = Document.objects.get(id=123) +chat = Chat.objects.create( + type='comments', + name=f'Комментарии: {document.title}', + context_object=document, + flags={'allow_anonymous': False} +) +``` + +### Комментарии к задаче + +```python +task = Task.objects.get(id=456) +chat = Chat.objects.create( + type='comments', + name=f'Обсуждение: {task.title}', + context_object=task, + flags={'notify_assignee': True} +) +``` + +### Комментарии к посту в соцсети + +```python +post = Post.objects.get(id=789) +chat = Chat.objects.create( + type='comments', + name='Comments', + context_object=post, + flags={'allow_public_comments': True, 'moderation_enabled': True} +) +``` + +## См. также + +- [LDAP_CONFIGURATION.md](LDAP_CONFIGURATION.md) - интеграция с LDAP +- [JS_MODULES_GUIDE.md](JS_MODULES_GUIDE.md) - frontend интеграция +- [REFACTORING_SUMMARY.md](REFACTORING_SUMMARY.md) - история рефакторинга