feat: requests detail view - stage 4: функциональность комментариев
- Добавлена функция удаления комментариев в views_front.py - Добавлена обработка удаления в URL routing - Расширен JavaScript модуль для работы с добавлением/удалением комментариев - Добавлена обработка формы добавления комментариев с JSON - Улучшена обработка ошибок при добавлении комментария - Добавлена инициализация emoji picker в модали - Обновлен счетчик комментариев при добавлении/удалении - Обновлены шаблоны для работы с данными из API - Добавлена поддержка перезагрузки содержимого модали после каждого действия
This commit is contained in:
parent
816aa8dbaa
commit
4d4a264b98
@ -5,6 +5,7 @@ from .views_front import (
|
||||
RequestDetailView,
|
||||
request_comments,
|
||||
request_comment_add,
|
||||
request_comment_delete,
|
||||
)
|
||||
|
||||
app_name = "requests"
|
||||
@ -18,4 +19,8 @@ urlpatterns = [
|
||||
request_comment_add,
|
||||
name="request_comment_add",
|
||||
),
|
||||
]
|
||||
path(
|
||||
"comments/<int:pk>/delete/<int:comment_id>/",
|
||||
request_comment_delete,
|
||||
name="request_comment_delete", ),
|
||||
]
|
||||
@ -396,3 +396,25 @@ def request_comment_add(request: HttpRequest, pk: int) -> JsonResponse:
|
||||
if not ok and st != 201:
|
||||
return JsonResponse(data or {"detail": f"HTTP {st}"}, status=st)
|
||||
return JsonResponse(data, status=201)
|
||||
|
||||
def request_comment_delete(request: HttpRequest, pk: int, comment_id: int) -> JsonResponse:
|
||||
"""Прокси: удаление комментария.
|
||||
|
||||
DELETE /api/v1/requests/{pk}/comments/{comment_id}/
|
||||
|
||||
Требует авторизации и проверяет права (только автор может удалить свой комментарий).
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"detail": "Method not allowed"}, status=405)
|
||||
if not request.user.is_authenticated:
|
||||
return JsonResponse({"detail": "Authentication required"}, status=401)
|
||||
|
||||
api = get_api_client(request)
|
||||
ok, data, st = _api_unpack(
|
||||
api.delete(f"v1/requests/{pk}/comments/{comment_id}/")
|
||||
)
|
||||
|
||||
if not ok:
|
||||
return JsonResponse(data or {"detail": f"HTTP {st}"}, status=st)
|
||||
|
||||
return JsonResponse({"detail": "Comment deleted"}, status=204)
|
||||
@ -111,6 +111,20 @@ export class RequestDetailModal {
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', (e) => this.handleCancel(e));
|
||||
}
|
||||
|
||||
// Обработчик для кнопок удаления комментариев
|
||||
const deleteCommentForms = this.container?.querySelectorAll('[action*="request_comment_delete"]');
|
||||
if (deleteCommentForms) {
|
||||
deleteCommentForms.forEach(form => {
|
||||
form.addEventListener('submit', (e) => this.handleDeleteComment(e));
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация emoji picker если доступна
|
||||
this.initEmojiPicker();
|
||||
|
||||
// Обновить счетчик комментариев
|
||||
this.updateCommentsCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,11 +142,98 @@ export class RequestDetailModal {
|
||||
return;
|
||||
}
|
||||
|
||||
// Показать состояние загрузки
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn?.textContent || 'Отправить';
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Отправка...';
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || ''
|
||||
},
|
||||
body: JSON.stringify({ text: text }),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Очистить форму
|
||||
textarea.value = '';
|
||||
|
||||
// Показать успешное сообщение
|
||||
if (submitBtn) {
|
||||
const originalClass = submitBtn.className;
|
||||
submitBtn.className = submitBtn.className.replace('btn-primary', 'btn-success');
|
||||
submitBtn.innerHTML = '<i class="bi-check-circle me-2"></i>Комментарий добавлен!';
|
||||
|
||||
// Вернуть кнопку в исходное состояние
|
||||
setTimeout(() => {
|
||||
submitBtn.className = originalClass;
|
||||
submitBtn.innerHTML = originalBtnText;
|
||||
submitBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Перезагрузить содержимое модали
|
||||
if (this.currentRequestId) {
|
||||
// Небольшая задержка для лучшего UX
|
||||
setTimeout(() => {
|
||||
this.loadData(this.currentRequestId);
|
||||
this.updateCommentsCount();
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
|
||||
// Восстановить кнопку при ошибке
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalBtnText;
|
||||
}
|
||||
|
||||
// Показать ошибку в UI
|
||||
const alertDiv = form.querySelector('.alert');
|
||||
if (alertDiv) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
|
||||
const errorAlert = document.createElement('div');
|
||||
errorAlert.className = 'alert alert-danger alert-dismissible fade show mb-2';
|
||||
errorAlert.innerHTML = `
|
||||
<strong>Ошибка:</strong> ${error.message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
form.parentNode.insertBefore(errorAlert, form);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик удаления комментария
|
||||
*/
|
||||
async handleDeleteComment(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('Вы уверены, что хотите удалить этот комментарий?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = e.target;
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || ''
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
@ -140,28 +241,42 @@ export class RequestDetailModal {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Очистить форму
|
||||
textarea.value = '';
|
||||
|
||||
// Перезагрузить содержимое модали
|
||||
if (this.currentRequestId) {
|
||||
await this.loadData(this.currentRequestId);
|
||||
this.updateCommentsCount();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting comment:', error);
|
||||
alert('Ошибка при добавлении комментария: ' + error.message);
|
||||
console.error('Error deleting comment:', error);
|
||||
alert('Ошибка при удалении комментария: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик отозвания заявления
|
||||
* Инициализировать emoji picker если доступен
|
||||
*/
|
||||
handleCancel(e) {
|
||||
e.preventDefault();
|
||||
initEmojiPicker() {
|
||||
const emojiPicker = this.container?.querySelector('emoji-picker');
|
||||
const textarea = this.container?.querySelector('textarea[name="text"]');
|
||||
|
||||
if (confirm('Вы уверены, что хотите отозвать это заявление?')) {
|
||||
// Перейти по ссылке или отправить форму
|
||||
window.location.href = e.target.href;
|
||||
if (emojiPicker && textarea) {
|
||||
emojiPicker.addEventListener('emoji-click', (e) => {
|
||||
textarea.value += e.detail.emoji.native;
|
||||
textarea.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить счетчик комментариев
|
||||
*/
|
||||
updateCommentsCount() {
|
||||
const commentsContainer = this.container?.querySelector('.d-flex.flex-column.gap-2.mb-3');
|
||||
const countSpan = this.container?.querySelector('.comments-count');
|
||||
|
||||
if (commentsContainer && countSpan) {
|
||||
const commentCount = commentsContainer.querySelectorAll('.bg-body-tertiary.border').length;
|
||||
countSpan.textContent = commentCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -110,25 +110,25 @@
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<div class="card-title">Комментарии</div>
|
||||
<div class="card-subtitle">Всего: {{ request_obj.comments.count }}</div>
|
||||
<div class="card-subtitle">Всего: <span class="comments-count">{{ comments|length }}</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card-body pt-0">
|
||||
{% if request_obj.comments.all %}
|
||||
{% if comments %}
|
||||
<div class="d-flex flex-column gap-2 mb-3">
|
||||
{% for c in request_obj.comments.all %}
|
||||
{% for c in comments %}
|
||||
<div class="bg-body-tertiary border rounded p-2">
|
||||
<div class="small d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="fw-semibold">{{ c.author.get_full_name|default:c.author.username }}</span>
|
||||
<span class="fw-semibold">{{ c.author.full_name|default:c.author_name }}</span>
|
||||
<span class="text-secondary">·</span>
|
||||
<time class="text-muted">{{ c.created_at|date:"d.m.Y H:i" }}</time>
|
||||
<time class="text-muted">{{ c.created_at }}</time>
|
||||
</div>
|
||||
|
||||
{# удалить — только автору комментария (права дополнительно проверяются во вьюхе) #}
|
||||
{% if c.author_id == request.user.id %}
|
||||
<form method="post" action="{% url 'requests_app:request_comment_delete' request_obj.pk c.pk %}">
|
||||
<form method="post" action="{% url 'requests_app:request_comment_delete' request_obj.pk c.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-sm btn-ghost text-danger" type="submit" title="Удалить">
|
||||
<i class="bi-trash"></i>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user