лишнее в приложении коммуникаци удалить

This commit is contained in:
lizerk 2026-03-10 13:28:46 +03:00
parent 8182199744
commit ca80321034
8 changed files with 0 additions and 1609 deletions

View File

@ -1,345 +0,0 @@
# Документация по обновлению мессенджера
## Обзор
Проведено полное обновление системы чатов EUSRR для превращения её в полнофункциональный корпоративный мессенджер.
---
## 📋 Реализованные возможности
### 1. **Модели данных** (Этапы 1-11) ✅
#### Обновленные модели:
**Chat**
- Новые поля: `name`, `description`, `avatar`, `created_by`, `include_all_employees`
- Расширенные типы: `private`, `group`, `department`, `channel`, `announcement`, `global`
**ChatUserSettings** (НОВАЯ)
- `is_pinned`, `pinned_at`, `pin_order` - закрепление чатов
- `notifications_enabled` - управление уведомлениями
- `custom_name` - персональное название чата
- `is_hidden` - скрытие чатов
**Message**
- `reply_to` - ответы на сообщения
- `is_edited`, `edited_at`, `edit_history` - редактирование с историей
- `is_deleted`, `deleted_at`, `deleted_by` - мягкое удаление
- `has_attachments` - флаг вложений
- `is_system`, `system_type`, `system_metadata` - системные сообщения
- `is_pinned`, `pinned_by`, `pinned_at` - закрепление важных сообщений
- `reactions` (JSON) - эмодзи-реакции
- `is_forwarded`, `is_cross_chat` - специальные типы
- `thread_root`, `thread_reply_count` - треды
**MessageAttachment** (НОВАЯ)
- Поддержка файлов: image, video, audio, file
- Автоматические превью для изображений
- Метаданные: размер, MIME-тип, оригинальное имя
**ForwardedMessage** (НОВАЯ)
- Отслеживание цепочек пересылки
- Сохранение оригинального автора и контента
- Счетчик количества пересылок
**MessageReply** (НОВАЯ)
- Расширенная информация об ответах
- Типы: inline, quote, thread
- Кросс-чат ответы
- Сохранение контекста при удалении оригинала
**ChatAccessPermission** (НОВАЯ)
- Гибкие уровни доступа: read, write, moderate
- Временные права с `expires_at`
- Аудит: кто предоставил, когда
**CrossChatMessage** (НОВАЯ)
- Отправка в чаты без членства
- Система модерации: pending, approved, rejected
- Заметки модератора
**ChatMembership** (НОВАЯ)
- Роли: owner, admin, moderator, member, guest
- Детальные права: отправка, добавление/удаление участников, закрепление
- История: joined_at, left_at, invited_by
**ChatReadState** (ОБНОВЛЕНА)
- `last_read_message` - явная связь
- `unread_mentions_count` - счетчик @упоминаний
- `unread_thread_replies_count` - счетчик ответов в тредах
- `is_typing`, `typing_updated_at` - индикатор набора текста
---
### 2. **API Endpoints** (Этап 12) ✅
Создано 15 REST API эндпоинтов в `communications/api_views.py`:
#### Управление чатами
- `POST /communications/api/chat/create/` - создание group/channel/announcement
- `POST /communications/api/chat/<id>/update/` - обновление названия/описания/аватара
- `POST /communications/api/chat/<id>/pin/` - закрепление/открепление чата
- `POST /communications/api/chat/<id>/notifications/` - управление уведомлениями
#### Действия с сообщениями
- `POST /communications/api/message/<id>/edit/` - редактирование с историей
- `POST /communications/api/message/<id>/delete/` - мягкое удаление
- `POST /communications/api/message/<id>/react/` - добавить реакцию
- `POST /communications/api/message/<id>/unreact/` - удалить реакцию
- `POST /communications/api/message/<id>/pin/` - закрепить сообщение
#### Вложения
- `POST /communications/api/attachment/upload/` - загрузка файлов
#### Пересылка и ответы
- `POST /communications/api/message/<id>/forward/` - пересылка с цепочкой
- `POST /communications/api/message/<id>/reply/` - ответ (inline/quote/thread)
#### Участники
- `POST /communications/api/chat/<id>/member/add/` - добавить участника
- `POST /communications/api/chat/<id>/member/remove/` - удалить участника
---
### 3. **WebSocket Consumers** (Этап 13) ✅
Обновлен `communications/consumers.py`:
#### ChatConsumer - обработка действий:
- `send_message` - отправка с поддержкой ответов
- `edit_message` - редактирование в реальном времени
- `delete_message` - удаление с уведомлением всех
- `add_reaction` / `remove_reaction` - реакции
- `typing` / `stop_typing` - индикатор набора текста
#### Real-time события:
- `chat.message` - новое сообщение
- `chat.message_edited` - сообщение изменено
- `chat.message_deleted` - сообщение удалено
- `chat.reaction_added` / `removed` - реакции обновлены
- `chat.user_typing` / `stopped_typing` - статус набора
#### Расширенная сериализация:
- Полная информация о сообщении
- Вложения с URL и превью
- Информация о пересылке
- Превью ответов
- Реакции всех пользователей
---
### 4. **Frontend (UI/UX)** (Этап 14) ✅
#### JavaScript модули:
**`chat-detail-enhanced.js`** - улучшенный интерфейс чата:
- Real-time обновления через WebSocket
- Редактирование сообщений с индикатором
- Удаление сообщений
- Быстрые реакции (6 эмодзи по наведению)
- Ответы на сообщения с превью
- Индикатор "печатает..."
- Автопереподключение WebSocket
- Отображение вложений и превью
- Информация о пересылке
**`chat-list-enhanced.js`** - улучшенный список чатов:
- Real-time обновление последних сообщений
- Закрепление чатов с перемещением наверх
- Контекстное меню (ПКМ):
- Закрепить/открепить
- Выключить уведомления
- Скрыть чат
- Отметить прочитанным
- Индикатор "печатает..." в списке
- Анимация обновления чатов
#### CSS стили:
**`chat-enhanced.css`** - стили для чата:
- Превью ответов с border-left
- Информация о пересылке
- Кнопки действий при наведении
- Стили реакций (активные/неактивные)
- Быстрые реакции на hover
- Отображение вложений
- Индикатор редактирования
- Значок закрепленного сообщения
- Системные сообщения
- Анимация новых сообщений
**`chat-list-enhanced.css`** - стили для списка:
- Контекстное меню
- Анимация обновления чата
- Стили закрепленных чатов (желтый фон)
- Значок закрепления
- Аватары чатов
- Групповые аватары
---
### 5. **Админ-панель** (Этап 12) ✅
Обновлен `communications/admin.py`:
- Админки для всех новых моделей
- Inline-редактирование вложений
- Inline-редактирование участников
- Фильтры и поиск
- Отображение статусов (edited, deleted, pinned)
---
## 🔧 Технические детали
### Миграции
Создано 12 миграций:
- `0006` - добавление полей в Chat
- `0007` - обновление типов чатов
- `0008` - модель ChatUserSettings
- `0009` - расширение Message (19 полей)
- `0010` - модель MessageAttachment
- `0011` - ForwardedMessage, MessageReply, ChatAccessPermission, CrossChatMessage
- `0012` - ChatMembership и обновление ChatReadState
### Зависимости
- Django Channels - WebSocket
- Pillow - обработка изображений
- Bootstrap 5 - UI компоненты
- Bootstrap Icons - иконки
### Структура файлов
```
communications/
├── models.py (обновлено - 10 моделей)
├── views.py (существующие view)
├── api_views.py (НОВОЕ - 15 эндпоинтов)
├── consumers.py (обновлено - real-time)
├── admin.py (обновлено - админки)
├── urls.py (обновлено - роуты API)
└── migrations/ (12 миграций)
static/
├── js/
│ ├── chat-detail-enhanced.js (НОВОЕ)
│ └── chat-list-enhanced.js (НОВОЕ)
└── css/
└── components/
├── chat-enhanced.css (НОВОЕ)
└── chat-list-enhanced.css (НОВОЕ)
```
---
## 📝 Использование
### Создание группового чата
```javascript
fetch('/communications/api/chat/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': getCsrfToken()
},
body: new URLSearchParams({
type: 'group',
name: 'Название группы',
description: 'Описание'
})
});
```
### Редактирование сообщения
```javascript
sendAction('edit_message', {
message_id: 123,
content: 'Новый текст'
});
```
### Добавление реакции
```javascript
sendAction('add_reaction', {
message_id: 123,
emoji: '👍'
});
```
### Закрепление чата
```javascript
fetch(`/communications/api/chat/${chatId}/pin/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': getCsrfToken()
},
body: 'pinned=true'
});
```
---
## 🎯 Следующие шаги (опционально)
### Возможные улучшения:
1. Упоминания (@username) с подсветкой и уведомлениями
2. Поиск по содержимому сообщений
3. Голосовые сообщения
4. Видеозвонки
5. Шифрование сообщений E2E
6. Экспорт истории чата
7. Боты и автоматизация
8. Rich text редактор (Markdown, форматирование)
9. Статусы пользователей (онлайн/офлайн/занят)
10. Push-уведомления
---
## ⚠️ Известные ограничения
1. **SQLite предупреждения** - deferrable constraints не поддерживаются (неблокирующее)
2. **Lint warnings** - несколько строк >79 символов (несущественно)
3. **UI требует подключения скриптов** - нужно добавить в шаблоны:
```django
{% block extra_js %}
<script src="{% static 'js/chat-detail-enhanced.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/components/chat-enhanced.css' %}">
{% endblock %}
```
---
## 📊 Статистика
- **Модели**: 10 (4 новые, 6 обновленных)
- **Миграции**: 12
- **API эндпоинты**: 15
- **WebSocket события**: 10 типов
- **JS файлы**: 2 новых модуля (~800 строк)
- **CSS файлы**: 2 новых файла (~400 строк)
- **Общий объем кода**: ~3500 строк
---
## ✅ Готовность к продакшену
### Выполнено:
- ✅ Все миграции применены
- ✅ Модели протестированы (создание/обновление)
- ✅ API эндпоинты реализованы
- ✅ WebSocket consumers обновлены
- ✅ Frontend код написан
- ✅ Админ-панель расширена
### Требуется:
- ⏳ Подключить новые JS/CSS файлы в шаблоны
- ⏳ Интеграционное тестирование
- ⏳ Тестирование производительности
- ⏳ Настройка прав доступа
- ⏳ Документация для пользователей
---
Автор: GitHub Copilot
Дата: 20 ноября 2025
Версия: 1.0

View File

@ -1,865 +0,0 @@
# Улучшения модели мессенджера
## 1. Добавить базовые поля в модель Chat
```python
class Chat(models.Model):
# Существующие поля...
type = models.CharField(max_length=16, choices=CHAT_TYPE_CHOICES)
# НОВЫЕ ПОЛЯ:
name = models.CharField(
max_length=255,
blank=True,
verbose_name="Название чата",
help_text="Для групповых/канальных чатов"
)
description = models.TextField(
blank=True,
verbose_name="Описание чата"
)
avatar = models.ImageField(
upload_to='chat_avatars/%Y/%m/',
null=True,
blank=True,
verbose_name="Аватар чата"
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.SET_NULL,
related_name='created_chats',
verbose_name="Создатель"
)
# Для гибкой настройки участников
include_all_employees = models.BooleanField(
default=False,
verbose_name="Включить всех сотрудников",
help_text="Для анонсов и общих чатов"
)
```
## 2. Создать модель для закрепления чатов (персональная настройка)
```python
class ChatUserSettings(models.Model):
"""Персональные настройки пользователя для чата"""
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='user_settings')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# Закрепление
is_pinned = models.BooleanField(default=False, verbose_name="Закреплен")
pinned_at = models.DateTimeField(null=True, blank=True, verbose_name="Время закрепления")
pin_order = models.IntegerField(default=0, verbose_name="Порядок закрепленных")
# Уведомления
notifications_enabled = models.BooleanField(default=True, verbose_name="Уведомления")
# Кастомное название (если пользователь переименовал)
custom_name = models.CharField(max_length=255, blank=True, verbose_name="Свое название")
# Скрыть чат
is_hidden = models.BooleanField(default=False, verbose_name="Скрыт")
class Meta:
unique_together = [('chat', 'user')]
ordering = ['-is_pinned', '-pinned_at']
```
## 3. Расширить типы чатов
```python
class Chat(models.Model):
CHAT_TYPE_CHOICES = [
('private', 'Личный'), # 1-на-1
('group', 'Групповой'), # Произвольная группа сотрудников
('department', 'Отдел'), # Привязан к отделу
('channel', 'Канал'), # Только админы пишут, все читают
('announcement', 'Объявления'), # Системный канал
]
```
## 4. Добавить роли участников
```python
class ChatMembership(models.Model):
"""Участие в чате с ролями"""
ROLE_CHOICES = [
('owner', 'Владелец'),
('admin', 'Администратор'),
('moderator', 'Модератор'),
('member', 'Участник'),
('readonly', 'Только чтение'),
]
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='memberships')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
joined_at = models.DateTimeField(auto_now_add=True)
invited_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.SET_NULL,
related_name='+'
)
# Разрешения
can_send_messages = models.BooleanField(default=True)
can_add_members = models.BooleanField(default=False)
can_edit_chat = models.BooleanField(default=False)
class Meta:
unique_together = [('chat', 'user')]
```
## 5. Расширить модель Message
```python
class Message(models.Model):
# Существующие поля...
chat = models.ForeignKey(Chat, related_name="messages", on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
# НОВЫЕ ПОЛЯ:
# Ответ на сообщение (упрощенная связь, детали в MessageReply)
reply_to = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='direct_replies',
help_text="Простая связь для быстрого доступа"
)
# Редактирование
is_edited = models.BooleanField(default=False)
edited_at = models.DateTimeField(null=True, blank=True)
edit_history = models.JSONField(
default=list,
blank=True,
help_text="История редактирования [{timestamp, old_content}, ...]"
)
# Удаление
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
deleted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Кто удалил (может отличаться от автора)"
)
# Вложения
has_attachments = models.BooleanField(default=False)
# Системные сообщения (вступление, выход и т.д.)
is_system = models.BooleanField(default=False)
system_type = models.CharField(max_length=50, blank=True)
# Типы: 'user_joined', 'user_left', 'chat_created', 'chat_renamed',
# 'user_promoted', 'user_demoted', 'settings_changed'
system_metadata = models.JSONField(
default=dict,
blank=True,
help_text="Дополнительные данные для системных сообщений"
)
# Важные/закрепленные
is_pinned = models.BooleanField(default=False)
pinned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
pinned_at = models.DateTimeField(null=True, blank=True)
# Реакции (можно хранить как JSON)
reactions = models.JSONField(default=dict, blank=True)
# Формат: {'👍': [user_id1, user_id2], '❤️': [user_id3]}
# Флаги для специальных типов сообщений
is_forwarded = models.BooleanField(default=False)
is_cross_chat = models.BooleanField(
default=False,
help_text="Сообщение отправлено не участником чата"
)
# Треды (для организации обсуждений)
thread_root = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='thread_messages',
help_text="Корневое сообщение треда"
)
thread_reply_count = models.IntegerField(default=0)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['chat', 'created_at']),
models.Index(fields=['chat', 'is_deleted', 'created_at']),
models.Index(fields=['author', 'created_at']),
models.Index(fields=['thread_root', 'created_at']),
models.Index(fields=['is_pinned', 'chat']),
]
```
## 6. Добавить модель вложений
```python
class MessageAttachment(models.Model):
ATTACHMENT_TYPE_CHOICES = [
('image', 'Изображение'),
('file', 'Файл'),
('video', 'Видео'),
('audio', 'Аудио'),
]
message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='attachments')
file = models.FileField(upload_to='chat_attachments/%Y/%m/%d/')
file_type = models.CharField(max_length=20, choices=ATTACHMENT_TYPE_CHOICES)
file_name = models.CharField(max_length=255)
file_size = models.BigIntegerField()
mime_type = models.CharField(max_length=100)
uploaded_at = models.DateTimeField(auto_now_add=True)
# Для изображений
thumbnail = models.ImageField(upload_to='chat_thumbnails/%Y/%m/%d/', null=True, blank=True)
```
## 6a. Улучшить модель пересылки сообщений
```python
class ForwardedMessage(models.Model):
"""Информация о пересланных сообщениях (поддержка цепочки пересылок)"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='forward_info',
help_text="Текущее пересланное сообщение"
)
# Оригинальное сообщение (первое в цепочке)
original_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='forwarded_copies',
help_text="Самое первое сообщение в цепочке пересылок"
)
# Непосредственный источник (откуда переслали)
immediate_source = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='+',
help_text="Сообщение, которое было непосредственно переслано"
)
original_chat = models.ForeignKey(
Chat,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
original_author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
forwarded_at = models.DateTimeField(auto_now_add=True)
forwarded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='forwarded_messages',
help_text="Кто переслал"
)
# Счетчик пересылок (для оригинала)
forward_count = models.IntegerField(default=0, help_text="Сколько раз переслали")
# Сохранить контент на случай удаления оригинала
preserved_content = models.TextField(blank=True)
preserved_author_name = models.CharField(max_length=255, blank=True)
class Meta:
indexes = [
models.Index(fields=['original_message']),
models.Index(fields=['forwarded_by', 'forwarded_at']),
]
```
## 6b. Добавить модель для отправки сообщений в чужие чаты
```python
class CrossChatMessage(models.Model):
"""Сообщения, отправленные в чаты, где пользователь не является участником"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='cross_chat_info'
)
sender = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='cross_chat_messages',
help_text="Отправитель (не член этого чата)"
)
target_chat = models.ForeignKey(
Chat,
on_delete=models.CASCADE,
related_name='cross_messages',
help_text="Чат-получатель"
)
# Разрешение на отправку
approved = models.BooleanField(default=False, help_text="Одобрено модератором")
approved_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Кто одобрил"
)
approved_at = models.DateTimeField(null=True, blank=True)
# Причина/контекст отправки
reason = models.CharField(
max_length=500,
blank=True,
help_text="Причина отправки сообщения в чужой чат"
)
# Режим отображения
DISPLAY_MODE_CHOICES = [
('guest', 'Гостевое (с пометкой "Внешнее сообщение")'),
('forwarded', 'Как пересланное'),
('announcement', 'Как объявление'),
]
display_mode = models.CharField(
max_length=20,
choices=DISPLAY_MODE_CHOICES,
default='guest'
)
sent_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['target_chat', 'approved']),
models.Index(fields=['sender', 'sent_at']),
]
```
## 7. Улучшить ChatReadState
```python
class ChatReadState(models.Model):
chat = models.ForeignKey("Chat", on_delete=models.CASCADE, related_name="read_states")
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
last_read_at = models.DateTimeField(null=True, blank=True)
last_read_message = models.ForeignKey(
Message,
null=True,
blank=True,
on_delete=models.SET_NULL,
help_text="Последнее прочитанное сообщение"
)
# Статус набора текста
is_typing = models.BooleanField(default=False)
typing_updated_at = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
```
## 8. Добавить модель для ответов на сообщения (расширенная)
```python
class MessageReply(models.Model):
"""Дополнительная информация об ответе на сообщение"""
message = models.OneToOneField(
Message,
on_delete=models.CASCADE,
related_name='reply_info',
help_text="Сообщение-ответ"
)
replied_to = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
related_name='replies',
help_text="Исходное сообщение, на которое отвечают"
)
# Для ответов через чаты (если отвечаешь в другом чате)
is_cross_chat_reply = models.BooleanField(default=False)
original_chat = models.ForeignKey(
Chat,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text="Чат оригинального сообщения (если отличается)"
)
# Сохраненный контент на случай удаления
preserved_text = models.TextField(blank=True)
preserved_author_name = models.CharField(max_length=255, blank=True)
# Тип ответа
REPLY_TYPE_CHOICES = [
('inline', 'Обычный ответ'),
('quote', 'Цитирование'),
('thread', 'В треде'),
]
reply_type = models.CharField(
max_length=20,
choices=REPLY_TYPE_CHOICES,
default='inline'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['replied_to']),
]
```
## 9. Добавить права доступа к чатам
```python
class ChatAccessPermission(models.Model):
"""Права пользователей на отправку сообщений в чужие чаты"""
chat = models.ForeignKey(
Chat,
on_delete=models.CASCADE,
related_name='access_permissions'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='chat_permissions'
)
# Типы доступа
PERMISSION_TYPE_CHOICES = [
('send_messages', 'Отправка сообщений'),
('read_only', 'Только чтение'),
('send_with_approval', 'Отправка с модерацией'),
]
permission_type = models.CharField(
max_length=30,
choices=PERMISSION_TYPE_CHOICES,
default='send_with_approval'
)
# Кто выдал право
granted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='+'
)
granted_at = models.DateTimeField(auto_now_add=True)
# Срок действия
expires_at = models.DateTimeField(
null=True,
blank=True,
help_text="Когда истекает право доступа"
)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = [('chat', 'user')]
indexes = [
models.Index(fields=['chat', 'is_active']),
models.Index(fields=['user', 'is_active']),
]
```
## 9. Убрать ограничения для гибкости
Удалить constraint'ы:
- `unique_main_global_chat`
- `unique_main_department_chat`
Заменить на поле `is_default` для автоматического выбора чата по умолчанию.
## 10. Добавить настройки чата
```python
class ChatSettings(models.Model):
"""Глобальные настройки чата"""
chat = models.OneToOneField(Chat, on_delete=models.CASCADE, related_name='settings')
# Кто может писать
only_admins_can_post = models.BooleanField(default=False)
# Кто может приглашать
members_can_invite = models.BooleanField(default=True)
# Автоудаление сообщений
auto_delete_messages_after = models.IntegerField(
null=True,
blank=True,
help_text="Автоматически удалять сообщения старше N дней"
)
# Максимум участников
max_members = models.IntegerField(null=True, blank=True)
# Запретить личные сообщения между участниками
disable_dm_between_members = models.BooleanField(default=False)
```
## Пример миграции (поэтапно)
### Этап 1: Базовые поля
```bash
# Добавить name, description, avatar в Chat
python manage.py makemigrations
python manage.py migrate
```
### Этап 2: Персональные настройки
```bash
# Создать ChatUserSettings
python manage.py makemigrations
python manage.py migrate
```
### Этап 3: Роли участников
```bash
# Создать ChatMembership
python manage.py makemigrations
python manage.py migrate
```
### Этап 4: Расширенные сообщения
```bash
# Добавить поля в Message, создать MessageAttachment
python manage.py makemigrations
python manage.py migrate
```
## Итоговая архитектура
```
Chat (базовая информация + настройки)
├── ChatUserSettings (персональные настройки каждого пользователя)
├── ChatMembership (роли и права участников)
├── ChatSettings (глобальные настройки чата)
├── ChatAccessPermission (права внешних пользователей на отправку сообщений)
├── Message
│ ├── MessageAttachment (файлы, изображения)
│ ├── ForwardedMessage (информация о пересылке с цепочкой)
│ ├── MessageReply (расширенная информация об ответах)
│ └── CrossChatMessage (сообщения из чужих чатов)
└── ChatReadState (статус прочтения)
```
## Новые возможности
### 1. Пересылка сообщений
- **Простая пересылка**: отправить сообщение в другой чат с указанием источника
- **Цепочка пересылок**: отслеживание всей истории пересылки (оригинал → промежуточные → текущее)
- **Счетчик пересылок**: сколько раз сообщение было переслано
- **Сохранение контента**: если оригинал удален, контент сохраняется
### 2. Ответы на сообщения
- **Внутри чата**: стандартный reply с цитированием
- **Кросс-чат ответы**: ответить на сообщение из другого чата
- **Типы ответов**: inline (обычный), quote (цитата), thread (тред)
- **Сохранение контекста**: текст и автор сохраняются при удалении оригинала
### 3. Отправка в чужие чаты
- **Режимы доступа**:
- `send_messages` - свободная отправка
- `read_only` - только чтение
- `send_with_approval` - модерация перед публикацией
- **Типы отображения**:
- `guest` - с пометкой "Внешнее сообщение"
- `forwarded` - как пересланное
- `announcement` - как объявление
- **Права доступа**: временные/постоянные, с указанием срока действия
- **Модерация**: одобрение администраторами чата
### 4. Треды (обсуждения)
- Организация дискуссий в отдельные ветки
- Счетчик ответов в треде
- Корневое сообщение треда
## Примеры использования
### Пересылка сообщения
```python
# В views или consumers
def forward_message(original_message_id, target_chat_id, user):
original = Message.objects.get(id=original_message_id)
# Создаем новое сообщение
forwarded_msg = Message.objects.create(
chat_id=target_chat_id,
author=user,
content=original.content,
is_forwarded=True,
has_attachments=original.has_attachments
)
# Копируем вложения
for att in original.attachments.all():
MessageAttachment.objects.create(
message=forwarded_msg,
file=att.file,
file_type=att.file_type,
file_name=att.file_name,
file_size=att.file_size,
mime_type=att.mime_type
)
# Находим оригинал в цепочке
if hasattr(original, 'forward_info'):
# Это уже пересланное - берем самый первый оригинал
root_original = original.forward_info.original_message
else:
root_original = original
# Создаем информацию о пересылке
ForwardedMessage.objects.create(
message=forwarded_msg,
original_message=root_original,
immediate_source=original,
original_chat=root_original.chat,
original_author=root_original.author,
forwarded_by=user,
preserved_content=root_original.content,
preserved_author_name=root_original.author.get_full_name()
)
# Увеличиваем счетчик
if hasattr(root_original, 'forward_info'):
root_original.forward_info.forward_count += 1
root_original.forward_info.save()
return forwarded_msg
```
### Отправка в чужой чат
```python
def send_to_external_chat(sender, chat_id, content, reason=''):
chat = Chat.objects.get(id=chat_id)
# Проверяем права доступа
permission = ChatAccessPermission.objects.filter(
chat=chat,
user=sender,
is_active=True
).first()
if not permission:
# Создаем запрос на отправку с модерацией
permission = ChatAccessPermission.objects.create(
chat=chat,
user=sender,
permission_type='send_with_approval',
granted_by=None
)
# Создаем сообщение
message = Message.objects.create(
chat=chat,
author=sender,
content=content,
is_cross_chat=True
)
# Создаем метаданные
CrossChatMessage.objects.create(
message=message,
sender=sender,
target_chat=chat,
approved=(permission.permission_type == 'send_messages'),
reason=reason,
display_mode='guest'
)
return message
```
### Ответ на сообщение из другого чата
```python
def reply_across_chats(user, original_msg_id, reply_content, target_chat_id):
original_msg = Message.objects.get(id=original_msg_id)
# Создаем ответ в целевом чате
reply_msg = Message.objects.create(
chat_id=target_chat_id,
author=user,
content=reply_content,
reply_to=original_msg
)
# Создаем расширенную информацию
MessageReply.objects.create(
message=reply_msg,
replied_to=original_msg,
is_cross_chat_reply=True,
original_chat=original_msg.chat,
preserved_text=original_msg.content,
preserved_author_name=original_msg.author.get_full_name(),
reply_type='quote'
)
return reply_msg
```
## Приоритет внедрения (для MVP)
1. **Высокий приоритет:**
- `name`, `avatar` в Chat
- `ChatUserSettings` (закрепление, уведомления)
- Расширение типов чатов (`group`, `channel`)
- `reply_to`, `is_edited`, `is_forwarded`, `is_cross_chat` в Message
- `MessageAttachment`
- `ForwardedMessage` (базовая пересылка)
- `MessageReply` (ответы в том же чате)
2. **Средний приоритет:**
- `ChatMembership` (роли)
- `ChatAccessPermission` (права на отправку в чужие чаты)
- `CrossChatMessage` (отправка в чужие чаты с модерацией)
- Кросс-чат ответы
- Реакции на сообщения
- Системные сообщения
- История редактирования
3. **Низкий приоритет:**
- Треды
- Автоудаление
- Расширенные разрешения
- Статус "печатает"
- Счетчики пересылок
## Изменения в UI/UX
После внедрения этих улучшений понадобится:
### Основные функции
- Модалка редактирования чата (название, аватар, описание)
- Контекстное меню для закрепления чатов
- Управление участниками (добавить/удалить/изменить роль)
- Загрузка файлов в сообщениях
- Ответы на сообщения (quote UI)
- Реакции на сообщения
- Редактирование/удаление своих сообщений
### Пересылка сообщений
- Кнопка "Переслать" в контекстном меню сообщения
- Модалка выбора чатов для пересылки (с поиском)
- Множественная пересылка (выбрать несколько чатов)
- Отображение "Переслано от [Имя]" в сообщении
- Ссылка на оригинальное сообщение (если доступно)
- Индикатор цепочки пересылок ("Переслано 5 раз")
### Ответы между чатами
- Возможность ответить в другом чате через контекстное меню
- Выбор целевого чата для ответа
- Цитата с указанием чата-источника
- Ссылка "Перейти к оригиналу" (если есть доступ)
### Отправка в чужие чаты
- Модалка "Отправить сообщение в чат"
- Поиск доступных чатов (даже без участия)
- Выбор режима отправки (гостевое/пересланное/объявление)
- Поле "Причина отправки"
- Статус модерации:
- "Ожидает одобрения" (желтый значок)
- "Одобрено" (зеленая галочка)
- "Отклонено" (красный крестик)
- Для модераторов: панель одобрения внешних сообщений
### Визуальные индикаторы
- **Пересланное сообщение**:
```
┌─────────────────────────────────┐
│ 📤 Переслано от Иван Петров │
│ Оригинал: Чат отдела IT │
├─────────────────────────────────┤
│ Текст сообщения... │
└─────────────────────────────────┘
```
- **Внешнее сообщение**:
```
┌─────────────────────────────────┐
│ 🔔 Внешнее сообщение │
│ От: Мария Сидорова │
│ Причина: Важное объявление │
├─────────────────────────────────┤
│ Текст сообщения... │
└─────────────────────────────────┘
```
- **Ответ из другого чата**:
```
┌─────────────────────────────────┐
│ Иван Петров → Чат отдела IT │
│ ↳ В ответ на сообщение из │
│ "Общий чат компании" │
├─────────────────────────────────┤
│ > Исходное сообщение... │
│ Ответ... │
└─────────────────────────────────┘
```
### Контекстное меню сообщения (расширенное)
- Ответить
- Ответить в другом чате →
- Переслать →
- В избранное
- В другой чат...
- В несколько чатов...
- Отправить в чужой чат →
- Скопировать
- Редактировать (свои)
- Удалить (свои/админ)
- Закрепить (админ)
- Пожаловаться
### Права и разрешения (админ панель чата)
- Настройки чата:
- Кто может писать (все/только участники/только админы)
- Разрешить внешние сообщения (да/нет/с модерацией)
- Кто может приглашать участников
- Управление внешними отправителями:
- Список пользователей с правами
- Добавить/удалить права
- Установить срок действия
- Модерация ожидающих сообщений

View File

@ -1,49 +0,0 @@
import datetime
from django.db import models
from django.db.models import Count, F, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils import timezone
from .models import Chat, ChatReadState, Message
def chat_unread_total(request):
"""
Считает общее количество непрочитанных сообщений пользователя.
Использует last_read_message_id (Telegram-style) вместо last_read_at.
"""
if not request.user.is_authenticated:
return {"chat_unread_total": 0}
user = request.user
# last_read_message_id для каждого чата текущего юзера
last_read_msg_sq = ChatReadState.objects.filter(
chat=models.OuterRef("pk"), user=user
).values("last_read_message_id")[:1]
# доступные юзеру чаты
deps = getattr(user, "departments", None)
deps_qs = deps.all() if deps is not None else []
chats_qs = (
Chat.objects.filter(
Q(type="global")
| Q(type="private", participants=user)
| Q(type="department", department__in=deps_qs)
)
.distinct()
.annotate(last_read_msg_id=Coalesce(Subquery(last_read_msg_sq), Value(0)))
.annotate(
unread=Count(
"messages",
filter=Q(messages__id__gt=F("last_read_msg_id"))
& ~Q(messages__author=user),
distinct=True,
)
)
)
total = chats_qs.aggregate(total=models.Sum("unread"))["total"] or 0
return {"chat_unread_total": total}

View File

@ -1,307 +0,0 @@
"""
Signals для автоматической генерации уведомлений в модуле Communications.
Обрабатывает события:
- Новое сообщение в чате
- Упоминание (@username) в сообщении
- Ответ на сообщение
- Добавление в чат
"""
import re
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Message, Chat, ChatUserSettings
from notifications.signals import notify
Employee = get_user_model()
def should_send_notification(chat, user):
"""
Проверяет, нужно ли отправлять уведомление пользователю для данного чата.
Args:
chat: Chat объект
user: Employee объект
Returns:
bool: True если нужно отправить уведомление, False если пользователь отключил уведомления
"""
try:
settings = ChatUserSettings.objects.get(chat=chat, user=user)
return settings.notifications_enabled
except ChatUserSettings.DoesNotExist:
# Если настройки не найдены, считаем что уведомления включены по умолчанию
return True
def get_users_with_notifications_enabled(chat, users):
"""
Получает словарь пользователей с их настройками уведомлений.
Оптимизированная версия - один запрос к БД вместо N.
Args:
chat: Chat объект
users: QuerySet или список пользователей
Returns:
dict: {user_id: notifications_enabled}
"""
if not users:
return {}
user_ids = [u.id for u in users]
# Один запрос для всех пользователей
settings = ChatUserSettings.objects.filter(
chat=chat,
user_id__in=user_ids
).values('user_id', 'notifications_enabled')
# Создаем словарь для быстрого доступа
settings_dict = {s['user_id']: s['notifications_enabled'] for s in settings}
# Для пользователей без настроек - по умолчанию True
return {uid: settings_dict.get(uid, True) for uid in user_ids}
@receiver(post_save, sender=Message)
def create_message_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании нового сообщения.
ОПТИМИЗАЦИЯ: Вся логика обработки вынесена в асинхронную Celery задачу,
чтобы не блокировать сохранение сообщения.
Обрабатывает:
1. Новое сообщение в чате (для всех участников кроме автора)
2. Упоминания (@username) в тексте
3. Ответ на сообщение (reply_to)
"""
if not created or instance.is_system or instance.is_deleted:
return
# Отправляем уведомления напрямую - channels.py автоматически отправит через Celery
_create_message_notifications_sync(instance)
def _create_message_notifications_sync(instance):
"""
Синхронная версия обработки уведомлений (fallback для разработки).
Используется только когда Celery недоступен.
"""
chat = instance.chat
author = instance.author
content = instance.content
# Получаем всех участников чата кроме автора
# Для announcement/channel/department используем get_participants
if chat.type in ['announcement', 'channel', 'department', 'global']:
participants = chat.get_participants.exclude(id=author.id)
else:
participants = chat.participants.exclude(id=author.id)
# ОПТИМИЗАЦИЯ: Загружаем настройки уведомлений ОДНИМ запросом для всех участников
participants_list = list(participants)
if not participants_list:
return # Нет получателей - выходим
notification_settings = get_users_with_notifications_enabled(chat, participants_list)
# 1. Обработка упоминаний (@username)
mentioned_users = extract_mentions(content)
mentioned_user_ids = []
if mentioned_users:
for email in mentioned_users:
try:
user = Employee.objects.get(email=email)
if user in participants_list and user.id != author.id:
if notification_settings.get(user.id, True):
notify.send(
sender=author,
recipient=user,
verb='chat_mention',
action_object=instance,
description=truncate_message(content, 100),
action_url='/messages',
data={
'title': f'Вас упомянул {author.get_full_name() or author.username}',
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'author_id': author.id,
}
)
mentioned_user_ids.append(user.id)
except Employee.DoesNotExist:
continue
# 2. Обработка ответов на сообщения
if instance.reply_to and instance.reply_to.author_id != author.id:
original_author = instance.reply_to.author
if original_author.id not in mentioned_user_ids:
if notification_settings.get(original_author.id, True):
notify.send(
sender=author,
recipient=original_author,
verb='chat_reply',
action_object=instance,
description=truncate_message(content, 100),
action_url='/messages',
data={
'title': f'{author.get_full_name() or author.username} ответил на ваше сообщение',
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'reply_to_id': instance.reply_to.id,
'author_id': author.id,
}
)
# 3. Обычное уведомление о новом сообщении
excluded_ids = set(mentioned_user_ids)
if instance.reply_to and instance.reply_to.author_id != author.id:
excluded_ids.add(instance.reply_to.author_id)
if chat.type == 'announcement':
notification_type = 'announcement_new_message'
title = f'Новое объявление от {author.get_full_name() or author.username}'
is_announcement = True
else:
notification_type = 'chat_new_message'
if chat.type == 'private':
title = f'Новое сообщение от {author.get_full_name() or author.username}'
else:
title = f'{author.get_full_name() or author.username} в {get_chat_name(chat)}'
is_announcement = False
for recipient in participants_list:
if recipient.id in excluded_ids:
continue
if not notification_settings.get(recipient.id, True):
continue
metadata = {
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': instance.id,
'author_id': author.id,
}
if is_announcement:
metadata['is_announcement'] = True
notify.send(
sender=author,
recipient=recipient,
verb=notification_type,
action_object=instance,
description=truncate_message(content, 150 if is_announcement else 100),
action_url='/messages',
data={**metadata, 'title': title},
)
@receiver(m2m_changed, sender=Chat.participants.through)
def create_chat_added_notifications(sender, instance, action, pk_set, **kwargs):
"""
Создает уведомления когда пользователя добавляют в чат.
"""
if action != 'post_add':
return
chat = instance
# Получаем информацию о том, кто добавил (если доступно)
# Это может быть последний изменивший или создатель чата
added_by = getattr(chat, 'created_by', None) or chat.participants.first()
# ОПТИМИЗАЦИЯ: Получаем всех новых пользователей одним запросом
new_users = Employee.objects.filter(id__in=pk_set)
# ОПТИМИЗАЦИЯ: Загружаем настройки одним запросом
notification_settings = get_users_with_notifications_enabled(chat, new_users)
# Создаем уведомления для новых участников
for user in new_users:
# Не отправляем уведомление тому, кто сам себя добавил
if added_by and user.id == added_by.id:
continue
# Проверяем настройки уведомлений (из предзагруженного словаря)
if not notification_settings.get(user.id, True):
continue
notify.send(
sender=added_by,
recipient=user,
verb='chat_added_to_chat',
action_object=chat,
description=f'Вы были добавлены в чат "{get_chat_name(chat)}"',
action_url='/messages',
data={
'title': 'Вас добавили в чат',
'added_by_id': added_by.id if added_by else None,
}
)
# ===== Вспомогательные функции =====
def extract_mentions(text):
"""
Извлекает email'ы из упоминаний вида @email.
Returns:
list: Список email'ов без символа @
"""
if not text:
return []
# Паттерн: @ + email формат
pattern = r'@([\w.+-]+@[\w.-]+\.[\w]+)'
mentions = re.findall(pattern, text)
return list(set(mentions)) # Уникальные значения
def truncate_message(text, max_length=100):
"""
Обрезает текст сообщения до указанной длины.
"""
if not text:
return ''
if len(text) <= max_length:
return text
return text[:max_length - 3] + '...'
def get_chat_name(chat):
"""
Возвращает название чата в зависимости от типа.
"""
if chat.name:
return chat.name
if chat.type == 'global':
return 'Глобальный чат'
if chat.type == 'department' and chat.department:
return f'Чат отдела: {chat.department.name}'
if chat.type == 'private':
# Для приватного чата можно вернуть имена участников
participants = chat.participants.all()[:2]
if participants:
names = [p.get_full_name() or p.username for p in participants]
return ', '.join(names)
return 'Чат'

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,38 +0,0 @@
# backend/communications/urls.py
from django.urls import path
from django.views.generic import TemplateView
from . import views
app_name = "communications"
urlpatterns = [
# UI Views
path("chats/", views.ChatListView.as_view(), name="chat_list"),
path(
"chats/<int:pk>/",
views.ChatDetailView.as_view(),
name="chat_detail"
),
path(
"test/websocket-events/",
TemplateView.as_view(template_name="test_websocket_events.html"),
name="test_websocket_events"
),
path(
"chats/start/<int:employee_pk>/",
views.start_private_chat,
name="start_private_chat"
),
path(
"chats/<int:pk>/mark-read/",
views.chat_mark_read,
name="chat_mark_read"
),
path(
"start/private/<int:employee_pk>/",
views.start_private_chat,
name="start_private_chat"
),
]

View File

@ -98,7 +98,6 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"communications.context_processors.chat_unread_total",
"eusrr_backend.context_processors.branding",
],
},

View File

@ -28,7 +28,6 @@ def serve_service_worker(request):
urlpatterns = [
path("admin/", admin.site.urls),
path("documents/", include("documents.urls", namespace="documents")),
path("communications/", include("communications.urls", namespace="communications")),
path("api/", include(("api.urls", "api"), namespace="api")),
# Service Worker должен быть в корне для правильного scope
path("sw.js", serve_service_worker, name="sw"),