Prototype: use partials

This commit is contained in:
2026-09-04 18:32:00 +02:00
parent 2832e76a31
commit ec5b964ac3
10 changed files with 96 additions and 205 deletions
+7 -17
View File
@@ -7,7 +7,7 @@
*/ */
// Must be loaded before Apline // Must be loaded before Apline
import htmx from "htmx.org"; import htmx, { HtmxResponse } from "htmx.org";
import "htmx.org/dist/ext/hx-alpine-compat.js"; import "htmx.org/dist/ext/hx-alpine-compat.js";
import "htmx.org/dist/ext/hx-prompt.js"; import "htmx.org/dist/ext/hx-prompt.js";
import "htmx.org/dist/ext/hx-download.js"; import "htmx.org/dist/ext/hx-download.js";
@@ -18,7 +18,6 @@ import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
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";
import { getErrorCallbacksExt } from "#core:htmx/error-callback";
import { import {
type NotificationPlugin, type NotificationPlugin,
notificationsPlugin as notifications, notificationsPlugin as notifications,
@@ -49,22 +48,13 @@ polyfillCountryFlagEmojis();
/** /**
* HTMX * HTMX
*/ */
document.body.addEventListener( document.body.addEventListener("htmx:before:request", (event) => {
"htmx:before:request" as keyof HTMLElementEventMap, event.target.ariaBusy = true;
(event) => { });
(event as CustomEvent).detail.ctx.target.ariaBusy = true;
},
);
document.body.addEventListener( document.body.addEventListener("htmx:before:swap", (event) => {
"htmx:before:swap" as keyof HTMLElementEventMap, event.target.ariaBusy = null;
(event) => { });
(event as CustomEvent).detail.ctx.target.ariaBusy = null;
},
);
const errorCallbackExt = getErrorCallbacksExt();
htmx.registerExtension(errorCallbackExt.name, errorCallbackExt.extension);
Object.assign(window, { htmx }); Object.assign(window, { htmx });
@@ -1,74 +0,0 @@
interface CustomHtmxExtension {
name: string;
extension: any;
}
export const getErrorCallbacksExt = () => {
const attrPrefix = "hx-callback-";
let htmxApi: { attributeValue: (arg0: HTMLElement, arg1: string) => string | null };
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?.attributeValue(elt, attr);
if (callback) {
return Function("src", "target", callback);
}
}
return () => {};
};
return {
name: "error-callbacks",
extension: {
init: (api: any) => {
htmxApi = api;
},
// biome-ignore lint/style/useNamingConvention: HTMX naming convention
htmx_response_error: (
elt: HTMLElement,
event: { ctx: any; cancelled: boolean },
) => {
if (event.cancelled) {
return false;
}
getCallback(elt, event.ctx.response.status)(elt, event.ctx.target);
return true;
},
},
} as CustomHtmxExtension;
};
@@ -1,17 +0,0 @@
<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>
@@ -1,47 +0,0 @@
<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>
@@ -0,0 +1,10 @@
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{% for comment in comments %}
{% include "pedagogy/fragments/ue_comment.jinja" %}
{% endfor %}
</section>
@@ -0,0 +1,10 @@
{% 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 %}
@@ -0,0 +1,27 @@
<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>
+27 -4
View File
@@ -1,7 +1,19 @@
{% from "pedagogy/macros.jinja" import display_star %} {% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %} {% if is_fragment %}
{% include "pedagogy/fragments/ue_detail.jinja" %}
<hx-partial id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</hx-partial>
<hx-partial id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</hx-partial>
<hx-partial id="comments" hx-swap="innerMorph">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</hx-partial>
{% else %} {% else %}
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
@@ -59,12 +71,23 @@
<br> <br>
{% include "pedagogy/fragments/ue_detail.jinja" %} <div id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</div>
{{ comments }} <br>
<div id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</div>
<div id="comments">
{% for comment in comments %}
{% include "pedagogy/fragments/ue_comment.jinja" %}
{% endfor %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% endif %} {% endif %}
-6
View File
@@ -31,7 +31,6 @@ from pedagogy.views import (
UECommentUpdateView, UECommentUpdateView,
UECreateView, UECreateView,
UEDeleteView, UEDeleteView,
UEDetailCommentsView,
UEDetailView, UEDetailView,
UEGuideView, UEGuideView,
UEModerationFormView, UEModerationFormView,
@@ -42,11 +41,6 @@ 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>/", UEDetailView.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( path(
"ue/<int:ue_id>/comment", "ue/<int:ue_id>/comment",
UECommentCreateView.as_view(), UECommentCreateView.as_view(),
+15 -40
View File
@@ -16,7 +16,7 @@
# details. # details.
# #
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
@@ -24,7 +24,6 @@
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 (
@@ -76,39 +75,12 @@ class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
"action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}) "action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}),
"object": self.ue,
} }
def form_valid(self, form): def get_success_url(self):
"""If the form is valid, save the associated model.""" return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue.id})
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):
self.object = self.get_object() # Needed if loaded with .as_fragment()
return super().get_context_data(**kwargs) | {
"comments": list(
self.object.comments.viewable_by(self.request.user)
.annotate_is_reported()
.select_related("author")
.order_by("-publish_date")
),
}
class UEDetailView( class UEDetailView(
@@ -122,13 +94,21 @@ class UEDetailView(
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
fragments = { fragments = {
"add_comment_form": UECommentCreateView, "add_comment_form": UECommentCreateView,
"comments": UEDetailCommentsView,
} }
def get_fragment_data(self): def get_fragment_data(self):
return { return {
"add_comment_form": {"ue_id": self.object.id}, "add_comment_form": {"ue_id": self.object.id},
"comments": {"ue_id": self.object.id}, }
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"comments": list(
self.object.comments.viewable_by(self.request.user)
.annotate_is_reported()
.select_related("author")
.order_by("-publish_date")
),
} }
@@ -144,11 +124,6 @@ class UECommentDetailView(PermissionRequiredMixin, DetailView):
super().get_queryset().viewable_by(self.request.user).annotate_is_reported() 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): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
"ue": getattr(self.object, "ue", None) "ue": getattr(self.object, "ue", None)