diff --git a/backend/api/v1/feed/serializers.py b/backend/api/v1/feed/serializers.py
index 797c3051..68e93495 100644
--- a/backend/api/v1/feed/serializers.py
+++ b/backend/api/v1/feed/serializers.py
@@ -161,6 +161,7 @@ class CommentSerializer(serializers.ModelSerializer):
created_at_display = serializers.DateTimeField(
source="created_at", format="%d.%m.%Y %H:%M", read_only=True
)
+ is_post_author = serializers.SerializerMethodField()
class Meta:
model = Comment
@@ -175,12 +176,19 @@ class CommentSerializer(serializers.ModelSerializer):
"attachment",
"created_at",
"created_at_display",
+ "is_post_author",
]
extra_kwargs = {
"post": {"write_only": True},
"text": {"required": False, "allow_blank": True},
}
+ def get_is_post_author(self, obj):
+ """Проверяет, является ли автор комментария автором поста"""
+ if not obj.author or not obj.post or not obj.post.author:
+ return False
+ return obj.author.id == obj.post.author.id
+
def validate(self, attrs):
text = (attrs.get("text") or "").strip()
if not (text or attrs.get("image") or attrs.get("attachment")):
diff --git a/backend/static/js/components/postDetailModal.js b/backend/static/js/components/postDetailModal.js
index b884e8eb..f6cdd2f7 100644
--- a/backend/static/js/components/postDetailModal.js
+++ b/backend/static/js/components/postDetailModal.js
@@ -1,16 +1,23 @@
/**
* postDetailModal.js
* Модуль для открытия деталей поста в модальном окне
+ * Полностью переработанная версия
*/
(function() {
'use strict';
+ // Константы
+ const INITIAL_COMMENTS_COUNT = 10;
+ const DEBUG = true;
+
+ // Состояние
let modal = null;
let modalElement = null;
+ let currentPost = null;
/**
- * Инициализация модального окна
+ * Инициализация
*/
function initPostDetailModal() {
modalElement = document.getElementById('postDetailModal');
@@ -21,7 +28,6 @@
modal = new bootstrap.Modal(modalElement);
- // Очистка при закрытии
modalElement.addEventListener('hidden.bs.modal', function() {
const contentDiv = document.getElementById('postDetailContent');
if (contentDiv) {
@@ -33,14 +39,14 @@
`;
}
+ currentPost = null;
});
- // Добавляем обработчики на ссылки постов
attachPostLinkHandlers();
}
/**
- * Получение CSRF токена из cookie
+ * Утилиты для работы с cookies
*/
function getCsrfToken() {
const cookies = document.cookie.split(';');
@@ -53,9 +59,6 @@
return null;
}
- /**
- * Получение JWT токена из cookie (для загрузки данных)
- */
function getAccessToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
@@ -68,197 +71,20 @@
}
/**
- * Загрузка деталей поста через API
+ * Утилиты
*/
- async function loadPostDetail(postId) {
- const accessToken = getAccessToken();
- const headers = {
- 'Accept': 'application/json'
- };
-
- if (accessToken) {
- headers['Authorization'] = `Bearer ${accessToken}`;
- }
-
- try {
- const response = await fetch(`/api/v1/posts/${postId}/`, {
- method: 'GET',
- headers: headers
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const post = await response.json();
- renderPostDetail(post);
- } catch (error) {
- console.error('Ошибка загрузки поста:', error);
- showError('Не удалось загрузить пост');
- }
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
}
- /**
- * Отображение деталей поста в модальном окне
- */
- function renderPostDetail(post) {
- const contentDiv = document.getElementById('postDetailContent');
-
- // Сохраняем ID автора для определения его комментариев
- const postAuthorId = post.author?.id;
-
- // Формируем HTML с кнопкой закрытия
- let html = '';
- html += '';
-
- // Заголовок с автором
- html += '';
-
- // Изображение
- if (post.image) {
- html += ``;
- }
-
- // Тело
- html += '';
- html += `
${post.title || 'Без названия'}
`;
- if (post.body) {
- html += `
${formatBody(post.body)}
`;
- }
- html += '
';
-
- // Футер с лайками и комментариями
- html += '';
-
- // Секция комментариев внутри карточки
- html += ''; // comments-section-wrapper
-
- html += '';
-
- contentDiv.innerHTML = html;
-
- // Прокручиваем к нижней части комментариев после того, как DOM обновится
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- const commentsContainer = document.getElementById('commentsListContainer');
- if (commentsContainer) {
- commentsContainer.scrollTop = commentsContainer.scrollHeight;
- }
- });
- });
-
- // Добавляем обработчики
- attachLikeHandlers();
+ function getAuthorId(authorObj) {
+ if (!authorObj) return null;
+ // API возвращает author.id
+ return authorObj.id || null;
}
- /**
- * Отображение комментария
- * @param {Object} comment - Объект комментария
- * @param {number} postAuthorId - ID автора поста
- */
- function renderComment(comment, postAuthorId) {
- const isAuthor = comment.author?.id === postAuthorId;
- const commentClass = isAuthor ? 'comment-item comment-item--author' : 'comment-item';
-
- let html = `
';
-
- return html;
- }
-
- /**
- * Форматирование даты
- */
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
@@ -276,43 +102,261 @@
}
}
- /**
- * Форматирование текста (простая обработка переносов строк)
- */
function formatBody(text) {
return text.replace(/\n/g, '
');
}
- /**
- * Показать ошибку
- */
- function showError(message) {
- const contentDiv = document.getElementById('postDetailContent');
- contentDiv.innerHTML = `
-
- ${message}
-
- `;
+ function pluralizeComments(count) {
+ const cases = [2, 0, 1, 1, 1, 2];
+ const titles = ['комментарий', 'комментария', 'комментариев'];
+ return titles[(count % 100 > 4 && count % 100 < 20) ? 2 : cases[(count % 10 < 5) ? count % 10 : 5]];
}
/**
- * Открыть модальное окно с постом
+ * Загрузка поста
*/
- function openPostModal(postId) {
- if (!modal) {
- console.error('Modal не инициализирован');
- return;
+ async function loadPostDetail(postId) {
+ const accessToken = getAccessToken();
+ const headers = { 'Accept': 'application/json' };
+
+ if (accessToken) {
+ headers['Authorization'] = `Bearer ${accessToken}`;
}
- modal.show();
- loadPostDetail(postId);
+ try {
+ const response = await fetch(`/api/v1/posts/${postId}/`, {
+ method: 'GET',
+ headers: headers
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const post = await response.json();
+
+ if (DEBUG) {
+ console.log('=== POST DEBUG ===');
+ console.log('Post:', post);
+ console.log('Post author:', post.author);
+ console.log('Post author ID:', getAuthorId(post.author), typeof getAuthorId(post.author));
+ if (post.comments && post.comments.length > 0) {
+ const firstComment = post.comments[0];
+ console.log('First comment:', firstComment);
+ console.log('Comment author:', firstComment.author);
+ console.log('Comment author ID:', getAuthorId(firstComment.author), typeof getAuthorId(firstComment.author));
+ }
+ console.log('==================');
+ }
+
+ currentPost = post;
+ renderPostDetail(post);
+ } catch (error) {
+ console.error('Ошибка загрузки поста:', error);
+ showError('Не удалось загрузить пост');
+ }
}
/**
- * Добавление обработчиков на ссылки постов
+ * Рендеринг
*/
+ function renderPostDetail(post) {
+ const contentDiv = document.getElementById('postDetailContent');
+ const postAuthorId = getAuthorId(post.author);
+
+ if (DEBUG) {
+ console.log('Rendering with postAuthorId:', postAuthorId);
+ }
+
+ let html = '';
+ html += '';
+ html += renderPostHeader(post);
+
+ if (post.image) {
+ html += ``;
+ }
+
+ html += '';
+ html += `
${escapeHtml(post.title || 'Без названия')}
`;
+ if (post.body) {
+ html += `
${formatBody(post.body)}
`;
+ }
+ html += '
';
+
+ html += renderPostFooter(post);
+ html += renderCommentsSection(post, postAuthorId);
+ html += '';
+
+ contentDiv.innerHTML = html;
+
+ if (post.comments && post.comments.length > 0) {
+ contentDiv.dataset.allComments = JSON.stringify(post.comments);
+ contentDiv.dataset.postAuthorId = postAuthorId || '';
+ }
+
+ scrollToBottom();
+ attachLikeHandlers();
+ attachLoadMoreCommentsHandler();
+ attachCommentFormHandler();
+ }
+
+ function renderPostHeader(post) {
+ let html = '';
+ return html;
+ }
+
+ function renderPostFooter(post) {
+ let html = '';
+ return html;
+ }
+
+ function renderCommentsSection(post, postAuthorId) {
+ let html = '';
+
+ return html;
+ }
+
+ function renderComment(comment, postAuthorId) {
+ // Используем флаг из API (приоритет) или fallback на сравнение ID
+ let isAuthor = false;
+
+ if (comment.hasOwnProperty('is_post_author')) {
+ // Используем флаг из API
+ isAuthor = comment.is_post_author;
+ } else {
+ // Fallback: сравниваем ID вручную
+ const commentAuthorId = getAuthorId(comment.author);
+ isAuthor = (commentAuthorId !== null && postAuthorId !== null &&
+ String(commentAuthorId) === String(postAuthorId));
+ }
+
+ if (DEBUG) {
+ console.log(`Comment by ${comment.author?.full_name}:`, {
+ is_post_author_flag: comment.is_post_author,
+ calculated: isAuthor,
+ comment_author_id: getAuthorId(comment.author),
+ post_author_id: postAuthorId
+ });
+ }
+
+ const commentClass = isAuthor ? 'comment-item comment-item--author' : 'comment-item';
+
+ let html = `';
+
+ return html;
+ }
+
+ function renderCommentForm(postId) {
+ let html = '';
+ return html;
+ }
+
+ /**
+ * Обработчики
+ */
+ function scrollToBottom() {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ const container = document.getElementById('commentsListContainer');
+ if (container) {
+ container.scrollTop = container.scrollHeight;
+ }
+ });
+ });
+ }
+
function attachPostLinkHandlers() {
- // Обработка кликов по ссылкам на посты
document.addEventListener('click', function(e) {
const link = e.target.closest('a[data-post-link]');
if (link) {
@@ -325,9 +369,83 @@
});
}
- /**
- * Обработчики лайков в модальном окне
- */
+ function attachLoadMoreCommentsHandler() {
+ const loadMoreBtn = document.querySelector('.load-more-comments');
+ if (!loadMoreBtn) return;
+
+ loadMoreBtn.addEventListener('click', function() {
+ const contentDiv = document.getElementById('postDetailContent');
+ const allComments = JSON.parse(contentDiv.dataset.allComments || '[]');
+ const postAuthorId = contentDiv.dataset.postAuthorId;
+ const container = document.getElementById('commentsListContainer');
+
+ if (!container || allComments.length === 0) return;
+
+ const scrollHeight = container.scrollHeight;
+ const scrollTop = container.scrollTop;
+
+ let html = '';
+ allComments.forEach(comment => {
+ html += renderComment(comment, postAuthorId);
+ });
+
+ container.innerHTML = html;
+
+ const newScrollHeight = container.scrollHeight;
+ const heightDiff = newScrollHeight - scrollHeight;
+ container.scrollTop = scrollTop + heightDiff;
+
+ this.remove();
+ });
+ }
+
+ function attachCommentFormHandler() {
+ const form = document.querySelector('.comment-form-modal');
+ if (!form) return;
+
+ form.addEventListener('submit', async function(e) {
+ e.preventDefault();
+ const textarea = this.querySelector('textarea[name="text"]');
+ const text = textarea.value.trim();
+
+ if (!text) return;
+
+ const postId = this.dataset.postId;
+ await submitComment(postId, text);
+ textarea.value = '';
+ });
+ }
+
+ async function submitComment(postId, text) {
+ const csrfToken = getCsrfToken();
+ if (!csrfToken) {
+ alert('Необходимо авторизоваться');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/v1/posts/${postId}/comments/`, {
+ method: 'POST',
+ headers: {
+ 'X-CSRFToken': csrfToken,
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: JSON.stringify({ text: text })
+ });
+
+ if (response.ok) {
+ // Перезагружаем пост
+ await loadPostDetail(postId);
+ } else {
+ alert('Ошибка при отправке комментария');
+ }
+ } catch (error) {
+ console.error('Ошибка:', error);
+ alert('Ошибка сети');
+ }
+ }
+
function attachLikeHandlers() {
const likeButtons = document.querySelectorAll('#postDetailContent .like-btn');
likeButtons.forEach(btn => {
@@ -339,9 +457,6 @@
});
}
- /**
- * Переключение лайка
- */
async function toggleLike(postId, button) {
const csrfToken = getCsrfToken();
if (!csrfToken) {
@@ -366,7 +481,6 @@
if (response.ok) {
const data = await response.json();
- // Обновляем иконку и счетчик
if (isLiked) {
icon.classList.remove('bi-heart-fill', 'text-danger');
icon.classList.add('bi-heart');
@@ -375,7 +489,6 @@
icon.classList.add('bi-heart-fill', 'text-danger');
}
- // Обновляем счетчик
const countText = button.childNodes[1];
if (countText) {
countText.textContent = ' ' + (data.likes_count || 0);
@@ -389,14 +502,32 @@
}
}
- // Инициализация при загрузке DOM
+ function showError(message) {
+ const contentDiv = document.getElementById('postDetailContent');
+ contentDiv.innerHTML = `
+
+ ${escapeHtml(message)}
+
+ `;
+ }
+
+ function openPostModal(postId) {
+ if (!modal) {
+ console.error('Modal не инициализирован');
+ return;
+ }
+
+ modal.show();
+ loadPostDetail(postId);
+ }
+
+ // Инициализация
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPostDetailModal);
} else {
initPostDetailModal();
}
- // Экспорт для использования извне
window.openPostModal = openPostModal;
})();
'; - html += '
Комментарии
'; - html += `(${post.comments.length})`; - html += '