Создание древовидных комментариев, добавление JavaScript
Без перезагрузки страницы
Сейчас после отправки комментария страница перезагружается и прокручивается в начало. Сделаем отправку через JavaScript: форма уходит в фоне, новый комментарий появляется в нужном месте дерева.
Сервер: JSON для AJAX
Одно представление обслуживает и обычную форму, и AJAX. Отличаем их по заголовку X-Requested-With, который мы сами добавим в fetch:
from django.http import JsonResponse
class CommentCreateView(LoginRequiredMixin, CreateView):
form_class = CommentCreateForm
def is_ajax(self):
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
def form_invalid(self, form):
if self.is_ajax():
return JsonResponse({"error": form.errors}, status=400)
return super().form_invalid(form)
def form_valid(self, form):
comment = form.save(commit=False)
comment.article_id = self.kwargs.get("pk")
comment.author = self.request.user
comment.parent_id = form.cleaned_data.get("parent")
comment.save()
if self.is_ajax():
return JsonResponse({
"is_child": comment.is_child_node(),
"id": comment.id,
"author": comment.author.username,
"parent_id": comment.parent_id,
"time_create": comment.time_create.strftime("%Y-%b-%d %H:%M:%S"),
"avatar": comment.author.profile.avatar.url,
"content": comment.content,
"get_absolute_url": comment.author.profile.get_absolute_url(),
}, status=200)
return redirect(comment.article.get_absolute_url())
def handle_no_permission(self):
return JsonResponse({"error": "Необходимо авторизоваться для добавления комментариев"}, status=400)
is_child_node()— метод MPTT: есть ли у комментария родитель;form_invalidдля AJAX возвращает ошибки JSON-ом со статусом 400.
Клиент: comments.js
templates/src/custom/js/comments.js, подключается внизу детальной страницы:
const commentForm = document.forms.commentForm;
const commentFormContent = commentForm.content;
const commentFormParentInput = commentForm.parent;
const commentFormSubmit = commentForm.commentSubmit;
const commentArticleId = commentForm.getAttribute("data-article-id");
commentForm.addEventListener("submit", createComment);
replyUser();
function replyUser() {
document.querySelectorAll(".btnReply").forEach((element) => {
element.addEventListener("click", replyComment);
});
}
function replyComment() {
const commentUsername = this.getAttribute("data-comment-username");
const commentMessageId = this.getAttribute("data-comment-id");
commentFormContent.value = `${commentUsername}, `;
commentFormParentInput.value = commentMessageId;
commentFormContent.focus();
}
async function createComment(event) {
event.preventDefault();
commentFormSubmit.disabled = true;
commentFormSubmit.innerText = "Ожидаем ответа сервера";
try {
const response = await fetch(commentForm.action, {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"X-Requested-With": "XMLHttpRequest",
},
body: new FormData(commentForm),
});
const comment = await response.json();
if (!response.ok) {
throw new Error(comment.error ? JSON.stringify(comment.error) : "Ошибка сервера");
}
const commentTemplate = `
<ul id="comment-thread-${comment.id}">
<li class="card border-0">
<div class="row">
<div class="col-md-2">
<img src="${comment.avatar}" style="width: 80px; height: 80px; object-fit: cover;" alt="${comment.author}"/>
</div>
<div class="col-md-10">
<div class="card-body">
<h6 class="card-title"><a href="${comment.get_absolute_url}">${comment.author}</a></h6>
<p class="card-text"></p>
<a class="btn btn-sm btn-dark btnReply" href="#commentForm"
data-comment-id="${comment.id}" data-comment-username="${comment.author}">Ответить</a>
<hr/>
<time>${comment.time_create}</time>
</div>
</div>
</div>
</li>
</ul>`;
if (comment.is_child) {
document.querySelector(`#comment-thread-${comment.parent_id}`)
.insertAdjacentHTML("beforeend", commentTemplate);
} else {
document.querySelector(".nested-comments")
.insertAdjacentHTML("beforeend", commentTemplate);
}
// текст вставляем через textContent — так он не выполнится как HTML
document.querySelector(`#comment-thread-${comment.id} .card-text`).textContent = comment.content;
commentForm.reset();
commentFormParentInput.value = null;
replyUser();
} catch (error) {
console.log(error);
alert(error.message);
} finally {
commentFormSubmit.disabled = false;
commentFormSubmit.innerText = "Добавить комментарий";
}
}
Что здесь происходит:
- Ответ. Кнопка «Ответить» пишет id комментария в скрытое поле
parentи подставляет имя автора в текст. - Отправка.
fetchсFormData— в теле те же поля, что отправила бы обычная форма, включаяcsrfmiddlewaretoken. ЗаголовокX-Requested-Withговорит серверу вернуть JSON. - Вставка. Ответ добавляется в
<ul>родителя, новый корневой комментарий — в конец дерева. - Безопасность. Текст комментария вставляем через
textContent: если пользователь напишет<script>, он покажется как текст, а не выполнится (XSS). - Блокируем кнопку на время запроса, чтобы не отправить комментарий дважды.
CSRF-токен в JavaScript
Переменная csrftoken объявляется один раз в main.html:
<script>const csrftoken = "{{ csrf_token }}";</script>
Или читается из cookie csrftoken (если CSRF_COOKIE_HTTPONLY = False, по умолчанию так и есть).
В форме добавьте атрибут для скрипта:
<form method="post" action="{% url 'comment_create_view' article.pk %}" id="commentForm" name="commentForm" data-article-id="{{ article.pk }}">
И подключите скрипт внизу шаблона статьи:
{% block script %}
<script src="{% static 'custom/js/comments.js' %}"></script>
{% endblock %}
Итог
- Одно представление: для AJAX —
JsonResponse, для обычной формы — редирект. fetch+FormData+ заголовкиX-CSRFTokenиX-Requested-With.- Кнопка «Ответить» заполняет скрытое
parent; ответ вставляется в ветку родителя. - Пользовательский текст вставляем через
textContent, а не черезinnerHTML.
Практика решено 0 из 5
В form_valid для AJAX-запросов верните JSON: если заголовок
self.request.headers.get("X-Requested-With") == "XMLHttpRequest", верните JsonResponse со словарём,
где "is_child": comment.is_child_node() и "id": comment.id. Иначе — редирект на статью.
def form_valid(self, form):
comment = form.save(commit=False)
comment.article_id = self.kwargs.get("pk")
comment.author = self.request.user
comment.parent_id = form.cleaned_data.get("parent")
comment.save()
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
return JsonResponse({
"is_child": comment.is_child_node(),
"id": comment.id,
})
return redirect(comment.article.get_absolute_url())
Отправьте форму комментария через fetch: метод POST, тело — new FormData(commentForm),
заголовки "X-CSRFToken": csrftoken и "X-Requested-With": "XMLHttpRequest". Адрес — commentForm.action.
async function createComment(event) {
event.preventDefault();
const response = await fetch(commentForm.action, {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"X-Requested-With": "XMLHttpRequest",
},
body: new FormData(commentForm),
});
const comment = await response.json();
}
Обработчик кнопки «Ответить»: запишите id комментария из data-comment-id в скрытое поле формы
(commentForm.parent.value = this.dataset.commentId) и поставьте фокус в поле текста
(commentForm.content.focus()).
function replyComment() {
commentForm.parent.value = this.dataset.commentId;
commentForm.content.focus();
}
Атрибут data-comment-id в HTML доступен в JavaScript как…
Что нужно, чтобы Django принял POST-запрос от fetch?
Войдите, чтобы отмечать пройденные уроки.