From 881943220b79f43d6641f91254bd31b503974bc9 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 27 Aug 2026 00:27:50 +0200 Subject: [PATCH] Htmx callback plugin and htmx based report view --- core/static/bundled/base-bundle-index.ts | 7 ++ core/static/bundled/htmx/error-callback.ts | 80 +++++++++++++++++++ pedagogy/static/pedagogy/css/pedagogy.scss | 11 ++- .../pedagogy/fragments/comment_report.jinja | 30 +++++++ .../pedagogy/fragments/ue_comment.jinja | 8 +- pedagogy/templates/pedagogy/macros.jinja | 2 +- pedagogy/views.py | 28 +++++-- 7 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 core/static/bundled/htmx/error-callback.ts create mode 100644 pedagogy/templates/pedagogy/fragments/comment_report.jinja diff --git a/core/static/bundled/base-bundle-index.ts b/core/static/bundled/base-bundle-index.ts index 65914f7a..6d4a016c 100644 --- a/core/static/bundled/base-bundle-index.ts +++ b/core/static/bundled/base-bundle-index.ts @@ -10,6 +10,10 @@ import sort from "@alpinejs/sort"; import Alpine from "alpinejs"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import htmx from "htmx.org"; +import { getErrorCallbacksExt } from "#core:htmx/error-callback"; + +("#core:htmx/error-callback"); + import { limitedChoices } from "#core:alpine/limited-choices"; import { expireOldStorage } from "#core:core/localstorage"; 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 }); /** diff --git a/core/static/bundled/htmx/error-callback.ts b/core/static/bundled/htmx/error-callback.ts new file mode 100644 index 00000000..052cc1c5 --- /dev/null +++ b/core/static/bundled/htmx/error-callback.ts @@ -0,0 +1,80 @@ +import type { HtmxExtension } from "htmx.org"; + +interface CustomHtmxExtension { + name: string; + extension: Partial; +} + +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; +}; diff --git a/pedagogy/static/pedagogy/css/pedagogy.scss b/pedagogy/static/pedagogy/css/pedagogy.scss index d4661e1b..7c780fa5 100644 --- a/pedagogy/static/pedagogy/css/pedagogy.scss +++ b/pedagogy/static/pedagogy/css/pedagogy.scss @@ -245,12 +245,6 @@ $pedagogy-white-text: #f0f0f0; .input-stars { margin-top: 20px; } - - .right { - display: flex; - justify-content: flex-end; - } - } .ue-details-container { @@ -523,4 +517,9 @@ details.accordion>.accordion-content { background-color: $white-color; border-color: $pedagogy-orange; border-right: none; +} + +.right { + display: flex; + justify-content: flex-end; } \ No newline at end of file diff --git a/pedagogy/templates/pedagogy/fragments/comment_report.jinja b/pedagogy/templates/pedagogy/fragments/comment_report.jinja new file mode 100644 index 00000000..d729f545 --- /dev/null +++ b/pedagogy/templates/pedagogy/fragments/comment_report.jinja @@ -0,0 +1,30 @@ +
+ {% csrf_token %} + {{ form.non_field_errors() }} + {{ form.reason.errors }} + {{ form.reason }} + + {# Hidden fields #} + {{ form.reporter }} + {{ form.comment }} + + + +

+ +

+
\ No newline at end of file diff --git a/pedagogy/templates/pedagogy/fragments/ue_comment.jinja b/pedagogy/templates/pedagogy/fragments/ue_comment.jinja index 6361e8d9..3a4e5cd6 100644 --- a/pedagogy/templates/pedagogy/fragments/ue_comment.jinja +++ b/pedagogy/templates/pedagogy/fragments/ue_comment.jinja @@ -47,10 +47,12 @@ {% endif %} {% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
{% csrf_token %}