лишнее в приложении коммуникаци удалить
This commit is contained in:
parent
8182199744
commit
ca80321034
@ -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
|
||||
@ -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 │
|
||||
│ ↳ В ответ на сообщение из │
|
||||
│ "Общий чат компании" │
|
||||
├─────────────────────────────────┤
|
||||
│ > Исходное сообщение... │
|
||||
│ Ответ... │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Контекстное меню сообщения (расширенное)
|
||||
- Ответить
|
||||
- Ответить в другом чате →
|
||||
- Переслать →
|
||||
- В избранное
|
||||
- В другой чат...
|
||||
- В несколько чатов...
|
||||
- Отправить в чужой чат →
|
||||
- Скопировать
|
||||
- Редактировать (свои)
|
||||
- Удалить (свои/админ)
|
||||
- Закрепить (админ)
|
||||
- Пожаловаться
|
||||
|
||||
### Права и разрешения (админ панель чата)
|
||||
- Настройки чата:
|
||||
- Кто может писать (все/только участники/только админы)
|
||||
- Разрешить внешние сообщения (да/нет/с модерацией)
|
||||
- Кто может приглашать участников
|
||||
- Управление внешними отправителями:
|
||||
- Список пользователей с правами
|
||||
- Добавить/удалить права
|
||||
- Установить срок действия
|
||||
- Модерация ожидающих сообщений
|
||||
|
||||
@ -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}
|
||||
@ -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 'Чат'
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -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"
|
||||
),
|
||||
]
|
||||
@ -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",
|
||||
],
|
||||
},
|
||||
|
||||
@ -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"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user