3 Commits
Author SHA1 Message Date
klmp200 881943220b Htmx callback plugin and htmx based report view 2026-08-27 18:38:17 +02:00
klmp200 c84cef8e55 Make ue comments a fragment 2026-08-26 21:56:39 +02:00
klmp200 13829c0945 Use HTMX in UE details 2026-08-26 21:56:39 +02:00
15 changed files with 542 additions and 254 deletions
+7
View File
@@ -10,6 +10,10 @@ import sort from "@alpinejs/sort";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org"; import htmx from "htmx.org";
import { getErrorCallbacksExt } from "#core:htmx/error-callback";
("#core:htmx/error-callback");
import { limitedChoices } from "#core:alpine/limited-choices"; import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage"; import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar"; import { default as navbar } from "#core:core/navbar";
@@ -57,6 +61,9 @@ document.body.addEventListener(
}, },
); );
const errorCallbackExt = getErrorCallbacksExt();
htmx.defineExtension(errorCallbackExt.name, errorCallbackExt.extension);
Object.assign(window, { htmx }); Object.assign(window, { htmx });
/** /**
@@ -0,0 +1,80 @@
import type { HtmxExtension } from "htmx.org";
interface CustomHtmxExtension {
name: string;
extension: Partial<HtmxExtension>;
}
export const getErrorCallbacksExt = () => {
const attrPrefix = "hx-callback-";
let htmxApi: {
[x: string]: any;
getClosestAttributeValue: any;
};
const getCallback = (elt: HTMLElement, responseCode: number) => {
if (!elt || !responseCode) {
return () => {};
}
const code = responseCode.toString();
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
const suffixes = [
code,
code.substring(0, 2) + "*",
code.substring(0, 2) + "x",
code.substring(0, 1) + "*",
code.substring(0, 1) + "x",
code.substring(0, 1) + "**",
code.substring(0, 1) + "xx",
"*",
"x",
"***",
"xxx",
];
if (code.startsWith("4") || code.startsWith("5")) {
suffixes.push("error");
}
for (const suffix of suffixes) {
const attr = attrPrefix + suffix;
const callback = htmxApi?.getClosestAttributeValue(elt, attr);
if (callback) {
return Function("src", "target", callback);
}
}
return () => {};
};
return {
name: "error-callbacks",
extension: {
init: (api: any) => {
htmxApi = api;
},
onEvent: (name: string, event: CustomEvent) => {
if (name !== "htmx:responseError") {
return true;
}
getCallback(event.detail.requestConfig.elt, event.detail.xhr.status)(
event.detail.requestConfig.elt,
htmxApi.getTarget(event.detail.requestConfig.elt),
);
return true;
},
},
} as CustomHtmxExtension;
};
+1 -1
View File
@@ -473,7 +473,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView): class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView):
model = User model = User
form_class = UserVisibilityForm form_class = UserVisibilityForm
template_name = "core/fragment/user_visibility.jinja" template_name = "core/fragments/user_visibility.jinja"
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
def get_form_kwargs(self): def get_form_kwargs(self):
+5 -1
View File
@@ -126,7 +126,11 @@ class UE(models.Model):
Returns: Returns:
True if the user has already posted a comment on this UE, else False. True if the user has already posted a comment on this UE, else False.
""" """
return self.comments.filter(author=user).exists() self._has_user_commented = getattr(self, "_has_user_commented", {})
self._has_user_commented[user] = self._has_user_commented.get(
user, self.comments.filter(author=user).exists()
)
return self._has_user_commented[user]
@cached_property @cached_property
def grade_global_average(self): def grade_global_average(self):
+9 -7
View File
@@ -205,14 +205,14 @@ $pedagogy-white-text: #f0f0f0;
grid-area: hours-the; grid-area: hours-the;
} }
#leave_comment_not_allowed { .leave-comment-not-allowed {
p { p {
text-align: center; text-align: center;
color: red; color: red;
} }
} }
#leave_comment { .leave-comment {
.leave-comment-grid-container { .leave-comment-grid-container {
display: grid; display: grid;
grid-template-columns: 270px auto; grid-template-columns: 270px auto;
@@ -245,10 +245,6 @@ $pedagogy-white-text: #f0f0f0;
.input-stars { .input-stars {
margin-top: 20px; margin-top: 20px;
} }
input[type="submit"] {
float: right;
}
} }
.ue-details-container { .ue-details-container {
@@ -410,8 +406,9 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px; padding-left: 10px;
} }
.actions { .action {
float: right; float: right;
margin-top: 0;
} }
} }
@@ -521,3 +518,8 @@ details.accordion>.accordion-content {
border-color: $pedagogy-orange; border-color: $pedagogy-orange;
border-right: none; border-right: none;
} }
.right {
display: flex;
justify-content: flex-end;
}
@@ -0,0 +1,30 @@
<form
hx-post="{{ request.get_full_path() }}"
hx-ext="error-callbacks"
hx-target="this"
hx-swap="outerHTML"
hx-disabled-elt="input[type='submit']"
hx-trigger="submit"
hx-callback-404="target.remove()"
>
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.reason.errors }}
{{ form.reason }}
{# Hidden fields #}
{{ form.reporter }}
{{ form.comment }}
<button
hx-get="{{ url('pedagogy:comment_detail', comment_id=comment_id) }}"
hx-target="closest form"
hx-swap="outerHTML"
>
{% trans %}Cancel{% endtrans %}
</button>
<p class="right" id="nique">
<input type="submit" value="{% trans %}Report{% endtrans %}" />
</p>
</form>
@@ -0,0 +1,83 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
<div id="comment-{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#comment-{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<button
class="btn btn-orange action"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
<i class="fa fa-pencil"></i> {% trans %}Edit{% endtrans %}
</button>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<form class="action"
hx-ext="error-callbacks"
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
hx-callback-404="document.getElementsByTagName('body')[0].dispatchEvent(new CustomEvent('CommentUpdate'));target.remove()"
>
{% csrf_token %}
<button class="btn btn-red action">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</form>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
@@ -0,0 +1,71 @@
<div class="leave-comment">
{% if form.is_creation %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{% endif %}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
{% endif %}
<form
hx-post="{{ action }}"
hx-target="closest .leave-comment"
hx-swap="outerHTML"
hx-disabled-elt="find input[type='submit']"
>
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p class="right">
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
</p>
</form>
{% if form.is_creation %}
</div>
</details>
{% endif %}
<br>
</div>
@@ -0,0 +1,17 @@
<section
hx-get="{{ url("pedagogy:ue_comments", ue_id=object.id) }}"
hx-swap="outerHTML"
hx-target="this"
hx-trigger="NewComment from:body"
>
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
{% for comment in comments %}
{% include "pedagogy/fragments/ue_comment.jinja" %}
{% endfor %}
</section>
@@ -0,0 +1,47 @@
<div
hx-get="{{ request.get_full_path() }}"
hx-swap="outerHTML"
hx-target="this"
hx-trigger="CommentUpdate from:body"
>
<div class="ue-details-container">
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
<br>
{% if object.has_user_already_commented(user) %}
<div class="leave-comment-not-allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
<br>
{% endif %}
{% if not object.has_user_already_commented(user) and user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
</div>
+10 -160
View File
@@ -1,7 +1,10 @@
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %} {% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %}
{% include "pedagogy/fragments/ue_detail.jinja" %}
{% else %}
{% extends "core/base.jinja" %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}"> <link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %} {% endblock %}
@@ -11,6 +14,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="pedagogy"> <div class="pedagogy">
<div id="ue_detail"> <div id="ue_detail">
<button onclick='(function(){ <button onclick='(function(){
@@ -55,166 +59,12 @@
<br> <br>
<div class="ue-details-container"> {% include "pedagogy/fragments/ue_detail.jinja" %}
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
<br> {{ comments }}
{% if object.has_user_already_commented(user) %}
<div id="leave_comment_not_allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
{% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{%endif%}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
<form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p><input type="submit" value="{% trans %}Comment{% endtrans %}" /></p>
</form>
</div>
</details>
{% endif %}
<br>
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
{% for comment in comments %}
<div id="{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<p class="actions">
<a href="{{ url('pedagogy:comment_update', comment_id=comment.id) }}">
{% trans %}Edit{% endtrans %}
</a>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<a href="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</p>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% endif %}
+20 -2
View File
@@ -24,12 +24,15 @@
from django.urls import path from django.urls import path
from pedagogy.views import ( from pedagogy.views import (
UECommentCreateView,
UECommentDeleteView, UECommentDeleteView,
UECommentDetailView,
UECommentReportCreateView, UECommentReportCreateView,
UECommentUpdateView, UECommentUpdateView,
UECreateView, UECreateView,
UEDeleteView, UEDeleteView,
UEDetailFormView, UEDetailCommentsView,
UEDetailView,
UEGuideView, UEGuideView,
UEModerationFormView, UEModerationFormView,
UEUpdateView, UEUpdateView,
@@ -38,7 +41,22 @@ from pedagogy.views import (
urlpatterns = [ urlpatterns = [
# Urls displaying the actual application for visitors # Urls displaying the actual application for visitors
path("", UEGuideView.as_view(), name="guide"), path("", UEGuideView.as_view(), name="guide"),
path("ue/<int:ue_id>/", UEDetailFormView.as_view(), name="ue_detail"), path("ue/<int:ue_id>/", UEDetailView.as_view(), name="ue_detail"),
path(
"ue/<int:ue_id>/comments",
UEDetailCommentsView.as_view(),
name="ue_comments",
),
path(
"ue/<int:ue_id>/comment",
UECommentCreateView.as_view(),
name="comment_create",
),
path(
"comment/<int:comment_id>/",
UECommentDetailView.as_view(),
name="comment_detail",
),
path( path(
"comment/<int:comment_id>/edit/", "comment/<int:comment_id>/edit/",
UECommentUpdateView.as_view(), UECommentUpdateView.as_view(),
+112 -33
View File
@@ -24,11 +24,13 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Exists, OuterRef from django.db.models import Exists, OuterRef
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView,
FormView, FormView,
TemplateView, TemplateView,
UpdateView, UpdateView,
@@ -36,7 +38,7 @@ from django.views.generic import (
from core.auth.mixins import PermissionOrAuthorRequiredMixin from core.auth.mixins import PermissionOrAuthorRequiredMixin
from core.models import Notification, User from core.models import Notification, User
from core.views import DetailFormView from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin
from pedagogy.forms import ( from pedagogy.forms import (
UECommentForm, UECommentForm,
UECommentModerationForm, UECommentModerationForm,
@@ -46,61 +48,120 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UEDetailFormView(PermissionRequiredMixin, DetailFormView): class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView):
"""Display every comment of an UE and detailed infos about it. model = UEComment
template_name = "pedagogy/fragments/ue_comment_form.jinja"
Allow to comment the UE.
"""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
form_class = UECommentForm form_class = UECommentForm
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.add_uecomment"
object = None # Avoid initialisation bug with FragmentMixin
@property
def ue(self):
if hasattr(self, "_ue"):
return self._ue
self._ue = get_object_or_404(UE, id=self.kwargs.get("ue_id"))
return self._ue
def has_permission(self): def has_permission(self):
if self.request.method == "POST" and not self.request.user.has_perm( if self.ue.has_user_already_commented(self.request.user):
"pedagogy.add_uecomment"
):
# if it's a POST request, the user is trying to add a new UEComment
# thus he also needs the "add_uecomment" permission
return False return False
return super().has_permission() return super().has_permission()
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.request.user.id kwargs["author_id"] = self.request.user.id
kwargs["ue_id"] = self.object.id kwargs["ue_id"] = self.ue.id
kwargs["is_creation"] = True kwargs["is_creation"] = True
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id})
}
def form_valid(self, form): def form_valid(self, form):
form.save() """If the form is valid, save the associated model."""
return super().form_valid(form) self.object = form.save()
response = HttpResponse(status=200)
response.headers["HX-Trigger"] = "NewComment, CommentUpdate"
return response
class UEDetailCommentsView(
PermissionRequiredMixin,
FragmentMixin,
DetailView,
):
"""Fragment view that display all comments"""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/fragments/ue_comments.jinja"
permission_required = "pedagogy.view_ue"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.object = self.get_object() # Needed if loaded with .as_fragment()
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
"comments": list( "comments": list(
self.object.comments.viewable_by(self.request.user) self.object.comments.viewable_by(self.request.user)
.annotate_is_reported() .annotate_is_reported()
.select_related("author") .select_related("author")
.order_by("-publish_date") .order_by("-publish_date")
) ),
} }
def get_success_url(self):
# once the new ue comment has been saved class UEDetailView(
# redirect to the same page we are currently PermissionRequiredMixin, UseFragmentsMixin, AllowFragment, DetailView
return self.request.path ):
"""Display every comment of an UE and detailed infos about it."""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
permission_required = "pedagogy.view_ue"
fragments = {
"add_comment_form": UECommentCreateView,
"comments": UEDetailCommentsView,
}
def get_fragment_data(self):
return {
"add_comment_form": {"ue_id": self.object.id},
"comments": {"ue_id": self.object.id},
}
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView): class UECommentDetailView(PermissionRequiredMixin, DetailView):
model = UEComment
pk_url_kwarg = "comment_id"
template_name = "pedagogy/fragments/ue_comment.jinja"
permission_required = "pedagogy.view_ue"
context_object_name = "comment"
def get_queryset(self):
return (
super().get_queryset().viewable_by(self.request.user).annotate_is_reported()
)
def dispatch(self, *args, **kwargs):
res: HttpResponse = super().dispatch(*args, **kwargs)
res.headers["HX-Trigger"] = "CommentUpdate"
return res
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"ue": getattr(self.object, "ue", None)
}
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
"""Allow edit of a given comment.""" """Allow edit of a given comment."""
model = UEComment model = UEComment
form_class = UECommentForm form_class = UECommentForm
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
template_name = "core/edit.jinja" template_name = "pedagogy/fragments/ue_comment_form.jinja"
permission_required = "pedagogy.change_uecomment" permission_required = "pedagogy.change_uecomment"
author_field = "author" author_field = "author"
@@ -111,11 +172,18 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
kwargs["is_creation"] = False kwargs["is_creation"] = False
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse(
"pedagogy:comment_update", kwargs={"comment_id": self.object.id}
)
}
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id}) return reverse("pedagogy:comment_detail", kwargs={"comment_id": self.object.id})
class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView): class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, DeleteView):
"""Allow to delete a given comment.""" """Allow to delete a given comment."""
model = UEComment model = UEComment
@@ -124,8 +192,13 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
permission_required = "pedagogy.delete_uecomment" permission_required = "pedagogy.delete_uecomment"
author_field = "author" author_field = "author"
def form_valid(self, form):
response = super().form_valid(form)
response.headers["HX-Trigger"] = "CommentUpdate"
return response
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id}) return reverse("pedagogy:comment_detail", kwargs={"comment_id": self.object.id})
class UEGuideView(PermissionRequiredMixin, TemplateView): class UEGuideView(PermissionRequiredMixin, TemplateView):
@@ -135,12 +208,12 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, CreateView): class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView):
"""Create a new report for an inappropriate comment.""" """Create a new report for an inappropriate comment."""
model = UECommentReport model = UECommentReport
form_class = UECommentReportForm form_class = UECommentReportForm
template_name = "core/edit.jinja" template_name = "pedagogy/fragments/comment_report.jinja"
permission_required = "pedagogy.add_uecommentreport" permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -153,6 +226,11 @@ class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
kwargs["comment_id"] = self.ue_comment.id kwargs["comment_id"] = self.ue_comment.id
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data() | {
"comment_id": self.ue_comment.id,
}
def form_valid(self, form): def form_valid(self, form):
resp = super().form_valid(form) resp = super().form_valid(form)
# Send a message to moderation admins # Send a message to moderation admins
@@ -168,11 +246,12 @@ class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
url=reverse("pedagogy:moderation"), url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION", type="PEDAGOGY_MODERATION",
) )
return resp return resp
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id}) return reverse(
"pedagogy:comment_detail", kwargs={"comment_id": self.ue_comment.id}
)
class UEModerationFormView(PermissionRequiredMixin, FormView): class UEModerationFormView(PermissionRequiredMixin, FormView):