Skip to content

Commit 2832e76

Browse files
committed
Htmx callback plugin and htmx based report view
1 parent 71ba949 commit 2832e76

7 files changed

Lines changed: 141 additions & 16 deletions

File tree

core/static/bundled/base-bundle-index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
1818
import { limitedChoices } from "#core:alpine/limited-choices";
1919
import { expireOldStorage } from "#core:core/localstorage";
2020
import { default as navbar } from "#core:core/navbar";
21+
import { getErrorCallbacksExt } from "#core:htmx/error-callback";
2122
import {
2223
type NotificationPlugin,
2324
notificationsPlugin as notifications,
@@ -62,6 +63,9 @@ document.body.addEventListener(
6263
},
6364
);
6465

66+
const errorCallbackExt = getErrorCallbacksExt();
67+
htmx.registerExtension(errorCallbackExt.name, errorCallbackExt.extension);
68+
6569
Object.assign(window, { htmx });
6670

6771
/**
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
interface CustomHtmxExtension {
2+
name: string;
3+
extension: any;
4+
}
5+
6+
export const getErrorCallbacksExt = () => {
7+
const attrPrefix = "hx-callback-";
8+
let htmxApi: { attributeValue: (arg0: HTMLElement, arg1: string) => string | null };
9+
10+
const getCallback = (elt: HTMLElement, responseCode: number) => {
11+
if (!elt || !responseCode) {
12+
return () => {};
13+
}
14+
15+
const code = responseCode.toString();
16+
17+
// '*' is the original syntax, as the obvious character for a wildcard.
18+
// The 'x' alternative was added for maximum compatibility with HTML
19+
// templating engines, due to ambiguity around which characters are
20+
// supported in HTML attributes.
21+
//
22+
// Start with the most specific possible attribute and generalize from
23+
// there.
24+
const suffixes = [
25+
code,
26+
27+
`${code.substring(0, 2)}*`,
28+
`${code.substring(0, 2)}x`,
29+
30+
`${code.substring(0, 1)}*`,
31+
`${code.substring(0, 1)}x`,
32+
`${code.substring(0, 1)}**`,
33+
`${code.substring(0, 1)}xx`,
34+
35+
"*",
36+
"x",
37+
"***",
38+
"xxx",
39+
];
40+
if (code.startsWith("4") || code.startsWith("5")) {
41+
suffixes.push("error");
42+
}
43+
44+
for (const suffix of suffixes) {
45+
const attr = attrPrefix + suffix;
46+
const callback = htmxApi?.attributeValue(elt, attr);
47+
if (callback) {
48+
return Function("src", "target", callback);
49+
}
50+
}
51+
52+
return () => {};
53+
};
54+
55+
return {
56+
name: "error-callbacks",
57+
extension: {
58+
init: (api: any) => {
59+
htmxApi = api;
60+
},
61+
// biome-ignore lint/style/useNamingConvention: HTMX naming convention
62+
htmx_response_error: (
63+
elt: HTMLElement,
64+
event: { ctx: any; cancelled: boolean },
65+
) => {
66+
if (event.cancelled) {
67+
return false;
68+
}
69+
getCallback(elt, event.ctx.response.status)(elt, event.ctx.target);
70+
return true;
71+
},
72+
},
73+
} as CustomHtmxExtension;
74+
};

pedagogy/static/pedagogy/css/pedagogy.scss

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,6 @@ $pedagogy-white-text: #f0f0f0;
168168
.input-stars {
169169
margin-top: 20px;
170170
}
171-
172-
.right {
173-
display: flex;
174-
justify-content: flex-end;
175-
}
176-
177171
}
178172

179173
.ue-details-container {
@@ -446,4 +440,9 @@ details.accordion>.accordion-content {
446440
background-color: $white-color;
447441
border-color: $pedagogy-orange;
448442
border-right: none;
443+
}
444+
445+
.right {
446+
display: flex;
447+
justify-content: flex-end;
449448
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<form
2+
hx-post="{{ request.get_full_path() }}"
3+
hx-ext="error-callbacks"
4+
hx-target="this"
5+
hx-swap="outerHTML"
6+
hx-disabled-elt="input[type='submit']"
7+
hx-trigger="submit"
8+
hx-callback-404="target.remove()"
9+
>
10+
{% csrf_token %}
11+
{{ form.non_field_errors() }}
12+
{{ form.reason.errors }}
13+
{{ form.reason }}
14+
15+
{# Hidden fields #}
16+
{{ form.reporter }}
17+
{{ form.comment }}
18+
19+
<button
20+
hx-get="{{ url('pedagogy:comment_detail', comment_id=comment_id) }}"
21+
hx-target="closest form"
22+
hx-swap="outerHTML"
23+
>
24+
{% trans %}Cancel{% endtrans %}
25+
</button>
26+
27+
<p class="right" id="nique">
28+
<input type="submit" value="{% trans %}Report{% endtrans %}" />
29+
</p>
30+
</form>

pedagogy/templates/pedagogy/fragments/ue_comment.jinja

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@
4747
{% endif %}
4848
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
4949
<form class="action"
50+
hx-ext="error-callbacks"
5051
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
5152
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
5253
hx-swap="outerHTML"
5354
hx-target="#comment-{{ comment.id }}"
55+
hx-callback-404="document.getElementsByTagName('body')[0].dispatchEvent(new CustomEvent('CommentUpdate'));target.remove()"
5456
>
5557
{% csrf_token %}
5658
<button class="btn btn-red action">
@@ -63,7 +65,11 @@
6365
<div class="comment-end-bar">
6466
<div class="report">
6567
<p>
66-
<a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">
68+
<a
69+
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
70+
hx-swap="outerHTML"
71+
hx-target="#comment-{{ comment.id }}"
72+
>
6773
{% trans %}Report this comment{% endtrans %}
6874
</a>
6975
</p>

pedagogy/templates/pedagogy/macros.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
<span class="grade-text"> {% trans %} not rated {% endtrans %} </span>
1414
{% endif %}
1515

16-
{%- endmacro %}
16+
{%- endmacro %}

pedagogy/views.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,19 @@ class UECommentDetailView(PermissionRequiredMixin, DetailView):
140140
context_object_name = "comment"
141141

142142
def get_queryset(self):
143-
return super().get_queryset().annotate_is_reported()
143+
return (
144+
super().get_queryset().viewable_by(self.request.user).annotate_is_reported()
145+
)
144146

145147
def dispatch(self, *args, **kwargs):
146148
res: HttpResponse = super().dispatch(*args, **kwargs)
147149
res.headers["HX-Trigger"] = "CommentUpdate"
148150
return res
149151

150152
def get_context_data(self, **kwargs):
151-
return super().get_context_data(**kwargs) | {"ue": self.object.ue}
153+
return super().get_context_data(**kwargs) | {
154+
"ue": getattr(self.object, "ue", None)
155+
}
152156

153157

154158
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
@@ -189,11 +193,13 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, Delete
189193
author_field = "author"
190194

191195
def form_valid(self, form):
192-
self.object.delete()
193-
response = HttpResponse(status=200)
196+
response = super().form_valid(form)
194197
response.headers["HX-Trigger"] = "CommentUpdate"
195198
return response
196199

200+
def get_success_url(self):
201+
return reverse("pedagogy:comment_detail", kwargs={"comment_id": self.object.id})
202+
197203

198204
class UEGuideView(PermissionRequiredMixin, TemplateView):
199205
"""UE guide main page."""
@@ -202,12 +208,12 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
202208
permission_required = "pedagogy.view_ue"
203209

204210

205-
class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
211+
class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView):
206212
"""Create a new report for an inappropriate comment."""
207213

208214
model = UECommentReport
209215
form_class = UECommentReportForm
210-
template_name = "core/edit.jinja"
216+
template_name = "pedagogy/fragments/comment_report.jinja"
211217
permission_required = "pedagogy.add_uecommentreport"
212218

213219
def dispatch(self, request, *args, **kwargs):
@@ -220,6 +226,11 @@ def get_form_kwargs(self):
220226
kwargs["comment_id"] = self.ue_comment.id
221227
return kwargs
222228

229+
def get_context_data(self, **kwargs):
230+
return super().get_context_data() | {
231+
"comment_id": self.ue_comment.id,
232+
}
233+
223234
def form_valid(self, form):
224235
resp = super().form_valid(form)
225236
# Send a message to moderation admins
@@ -235,11 +246,12 @@ def form_valid(self, form):
235246
url=reverse("pedagogy:moderation"),
236247
type="PEDAGOGY_MODERATION",
237248
)
238-
239249
return resp
240250

241251
def get_success_url(self):
242-
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id})
252+
return reverse(
253+
"pedagogy:comment_detail", kwargs={"comment_id": self.ue_comment.id}
254+
)
243255

244256

245257
class UEModerationFormView(PermissionRequiredMixin, FormView):

0 commit comments

Comments
 (0)