12 KiB
12 KiB
Анализ: Загрузка и встраивание опросов (polls) в DOM
🔍 Текущая архитектура
Backend: Сериализация опросов
Файл: backend/communications/consumers.py
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
ДО исправления:
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>
`;
}
ПОСЛЕ исправления:
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
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
ДО исправления:
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);
}
}
ПОСЛЕ исправления:
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)
{
"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)
<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: Создание опроса
- Откройте чат
- Нажмите "Создать голосование"
- Заполните вопрос и варианты
- Отправьте
- Ожидается: Опрос появляется в чате с кнопками для голосования
Сценарий 2: Редактирование сообщения с опросом
- Создайте сообщение с опросом
- Отредактируйте текст сообщения (сам опрос редактировать нельзя)
- Ожидается:
- Текст обновлён ✅
- Опрос остался на месте ✅
- Кнопки опроса работают ✅
Сценарий 3: Голосование
- Откройте сообщение с опросом
- Нажмите на вариант ответа
- Ожидается:
- Голос зарегистрирован
- Опрос обновлён с результатами
- Показаны проценты
📝 Итог
✅ Опросы загружаются через serialize_message в payload
✅ MessageRenderer генерирует HTML через buildPollHtml()
✅ ChatPoll инициализирует через refreshPoll()
✅ При редактировании опросы сохраняются через reinitMessageComponents()
Все проблемы решены! 🎉