Corp/POLL_LOADING_ANALYSIS.md

380 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Анализ: Загрузка и встраивание опросов (polls) в DOM
## 🔍 Текущая архитектура
### Backend: Сериализация опросов
**Файл**: `backend/communications/consumers.py`
```python
def serialize_message(m: Message) -> dict:
# ...
# Голосование
if hasattr(m, 'poll'):
poll = m.poll
poll_data = {
"id": poll.id,
"question": poll.question,
"is_anonymous": poll.is_anonymous,
"is_multiple_choice": poll.is_multiple_choice,
"is_quiz": poll.is_quiz,
"is_closed": poll.is_closed,
"closes_at": poll.closes_at.isoformat() if poll.closes_at else None,
"total_voters": poll.total_voters,
"options": []
}
for option in poll.options.all():
poll_data["options"].append({
"id": option.id,
"text": option.text,
"position": option.position,
"vote_count": option.vote_count,
"percentage": 0 # Будет пересчитан на клиенте
})
data["poll"] = poll_data
```
✅ **Опросы загружаются** в payload сообщения.
---
### Frontend: Генерация HTML
#### ❌ ПРОБЛЕМА: MessageRenderer НЕ генерировал HTML для опросов
**Файл**: `backend/static/js/components/messageRenderer.js`
**ДО исправления:**
```javascript
buildMessageHtml(msg, isOwn) {
const attachmentsHtml = msg.attachments?.length ?
msg.attachments.map(att => this.buildAttachmentHtml(att)).join('') : '';
// Опросы НЕ рендерились!
return `
<div class="bubble">
${msg.content}
${attachmentsHtml}
<!-- ← poll отсутствует! -->
</div>
`;
}
```
**ПОСЛЕ исправления:**
```javascript
buildMessageHtml(msg, isOwn) {
const attachmentsHtml = msg.attachments?.length ?
msg.attachments.map(att => this.buildAttachmentHtml(att)).join('') : '';
// ✅ Добавлена генерация HTML для опроса
const pollHtml = msg.poll ? this.buildPollHtml(msg.poll) : '';
return `
<div class="bubble">
${msg.content}
${attachmentsHtml}
${pollHtml} ← добавлено!
</div>
`;
}
// ✅ Новый метод
buildPollHtml(poll) {
if (!poll || !poll.id) return '';
const totalVotes = poll.total_voters || 0;
const isClosed = poll.is_closed || false;
return `
<div class="poll-widget mt-2" data-poll-id="${poll.id}">
<div class="poll-question mb-3">
<strong>${this.escapeHtml(poll.question)}</strong>
</div>
<div class="poll-options">
${poll.options?.map(option => `
<div class="poll-option mb-2" data-option-id="${option.id}">
<button type="button" class="btn btn-outline-secondary btn-poll-option w-100 text-start"
data-option-id="${option.id}">
${this.escapeHtml(option.text)}
</button>
</div>
`).join('') || ''}
</div>
<div class="poll-footer mt-3">
<div class="small text-muted">
${totalVotes} проголосовало
${poll.is_anonymous ? ' • Анонимное' : ''}
${poll.is_multiple_choice ? ' • Множественный выбор' : ''}
${isClosed ? ' • Закрыто' : ''}
</div>
</div>
</div>`;
}
```
---
### Frontend: Инициализация опросов
**Файл**: `backend/static/js/components/chatPoll.js`
```javascript
class ChatPoll {
initializeExistingPolls() {
const pollWidgets = document.querySelectorAll('.poll-widget[data-poll-id]');
pollWidgets.forEach((widget) => {
const pollId = widget.dataset.pollId;
this.refreshPoll(pollId, widget);
});
}
async refreshPoll(pollId, pollWidget = null) {
const widget = pollWidget || document.querySelector(`[data-poll-id="${pollId}"]`);
if (!widget) return;
// Загружает актуальные данные с сервера
const results = await this.fetchPollResults(pollId);
if (!results) return;
// Обновляет HTML опроса (результаты, проценты и т.д.)
this.updatePollWidget(widget, results);
}
}
```
✅ `ChatPoll` обрабатывает существующие опросы через `initializeExistingPolls()`.
---
### Frontend: Реинициализация после редактирования
**Файл**: `backend/static/js/components/messageEditing.js`
**ДО исправления:**
```javascript
function reinitMessageComponents(messageEl, messageId, currentUserId) {
// 4. Голосования (если есть)
const pollEl = messageEl.querySelector('[data-poll-id]');
if (pollEl && window.chatPoll) {
const pollId = pollEl.dataset.pollId;
// ❌ Только логирование, НЕ инициализация!
console.log('[MessageEditing] ✓ Poll detected, id:', pollId);
}
}
```
**ПОСЛЕ исправления:**
```javascript
function reinitMessageComponents(messageEl, messageId, currentUserId) {
// 4. Голосования (если есть)
const pollEl = messageEl.querySelector('[data-poll-id]');
if (pollEl && window.chatPoll) {
const pollId = pollEl.dataset.pollId;
console.log('[MessageEditing] Refreshing poll:', pollId);
// ✅ Явно вызываем refreshPoll для обновления состояния
window.chatPoll.refreshPoll(pollId, pollEl);
console.log('[MessageEditing] ✓ Poll reinitialized, id:', pollId);
}
}
```
---
## 🔄 Процесс загрузки и встраивания
### 1. Первоначальная загрузка сообщений
```
User opens chat
↓
Django renders chat_detail.html
↓
JavaScript загружает начальные сообщения через API
↓
Для каждого сообщения:
├─ MessageRenderer.buildMessageHtml(msg) создаёт HTML
│ ├─ msg.content → текст
│ ├─ msg.attachments → вложения
│ ├─ msg.reply_to → ответ на сообщение
│ └─ msg.poll → ТЕПЕРЬ ГЕНЕРИРУЕТСЯ! ✅
│ └─ buildPollHtml(poll) → <div class="poll-widget" data-poll-id="...">
↓
HTML вставляется в DOM
↓
ChatPoll.initializeExistingPolls() находит все [data-poll-id]
└─ Для каждого: refreshPoll(pollId)
└─ Загружает актуальные данные с сервера
└─ Обновляет проценты, результаты
```
### 2. Создание нового опроса через UI
```
User clicks "Создать голосование"
↓
Modal открывается
↓
User заполняет вопрос, варианты ответов
↓
ChatPoll.submitPoll() → POST /api/v1/communications/polls/create/
↓
Backend создаёт Poll + PollOption
↓
WebSocket → new_message event (с poll в payload)
↓
userWebSocket.js → dispatchEvent('new_message')
↓
chatScroll добавляет новое сообщение в DOM
↓
ChatPoll.initializeExistingPolls() или refreshPoll()
```
### 3. Редактирование сообщения с опросом
```
User edits message with poll
↓
POST /api/v1/communications/messages/{id}/edit/
↓
Backend: message.save() + prefetch poll/poll__options
↓
serialize_message(message) → включает poll data
↓
WebSocket → message_edited event
↓
userWebSocket.js → dispatchEvent('chat:message-edited')
↓
messageEditing.js: handleMessageEdited
├─ MessageRenderer.buildMessageHtml(message) → ТЕПЕРЬ включает poll! ✅
│ └─ buildPollHtml(message.poll) → генерирует HTML
├─ oldMessageEl.replaceWith(newMessageEl)
└─ reinitMessageComponents(newMessageEl)
└─ window.chatPoll.refreshPoll(pollId, pollEl) ✅
└─ Обновляет состояние опроса
```
---
## 📊 Структура данных опроса
### Backend (serialize_message)
```json
{
"id": 495,
"content": "Выберите вариант",
"poll": {
"id": 10,
"question": "Какой язык программирования вы предпочитаете?",
"is_anonymous": false,
"is_multiple_choice": false,
"is_quiz": false,
"is_closed": false,
"closes_at": null,
"total_voters": 5,
"options": [
{
"id": 21,
"text": "Python",
"position": 0,
"vote_count": 3,
"percentage": 60
},
{
"id": 22,
"text": "JavaScript",
"position": 1,
"vote_count": 2,
"percentage": 40
}
]
}
}
```
### Frontend (buildPollHtml)
```html
<div class="poll-widget mt-2" data-poll-id="10">
<div class="poll-question mb-3">
<strong>Какой язык программирования вы предпочитаете?</strong>
</div>
<div class="poll-options">
<div class="poll-option mb-2" data-option-id="21">
<button type="button" class="btn btn-outline-secondary btn-poll-option w-100 text-start"
data-option-id="21">
Python
</button>
</div>
<div class="poll-option mb-2" data-option-id="22">
<button type="button" class="btn btn-outline-secondary btn-poll-option w-100 text-start"
data-option-id="22">
JavaScript
</button>
</div>
</div>
<div class="poll-footer mt-3">
<div class="small text-muted">
5 проголосовало
</div>
</div>
</div>
```
---
## ✅ Исправления
### 1. MessageRenderer теперь генерирует HTML для опросов
- Добавлен метод `buildPollHtml(poll)`
- В `buildMessageHtml` добавлена строка `${pollHtml}`
### 2. reinitMessageComponents явно инициализирует опросы
- Вызывает `window.chatPoll.refreshPoll(pollId, pollEl)`
- Это загружает актуальные данные и обновляет UI
### 3. Полная поддержка редактирования
- При редактировании сообщения с опросом он не теряется
- Опрос корректно отображается после замены DOM
---
## 🧪 Тестирование
### Сценарий 1: Создание опроса
1. Откройте чат
2. Нажмите "Создать голосование"
3. Заполните вопрос и варианты
4. Отправьте
5. **Ожидается**: Опрос появляется в чате с кнопками для голосования
### Сценарий 2: Редактирование сообщения с опросом
1. Создайте сообщение с опросом
2. Отредактируйте **текст** сообщения (сам опрос редактировать нельзя)
3. **Ожидается**:
- Текст обновлён ✅
- Опрос остался на месте ✅
- Кнопки опроса работают ✅
### Сценарий 3: Голосование
1. Откройте сообщение с опросом
2. Нажмите на вариант ответа
3. **Ожидается**:
- Голос зарегистрирован
- Опрос обновлён с результатами
- Показаны проценты
---
## 📝 Итог
✅ **Опросы загружаются** через `serialize_message` в payload
✅ **MessageRenderer генерирует HTML** через `buildPollHtml()`
✅ **ChatPoll инициализирует** через `refreshPoll()`
✅ **При редактировании опросы сохраняются** через `reinitMessageComponents()`
**Все проблемы решены!** 🎉