Skip to content

Commit 97a3ad4

Browse files
committed
Htmx callback plugin and htmx based report view
1 parent c84cef8 commit 97a3ad4

7 files changed

Lines changed: 155 additions & 17 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import sort from "@alpinejs/sort";
1010
import Alpine from "alpinejs";
1111
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
1212
import htmx from "htmx.org";
13+
import { getErrorCallbacksExt } from "#core:htmx/error-callback";
14+
15+
("#core:htmx/error-callback");
16+
1317
import { limitedChoices } from "#core:alpine/limited-choices";
1418
import { expireOldStorage } from "#core:core/localstorage";
1519
import { default as navbar } from "#core:core/navbar";
@@ -57,6 +61,9 @@ document.body.addEventListener(
5761
},
5862
);
5963

64+
const errorCallbackExt = getErrorCallbacksExt();
65+
htmx.defineExtension(errorCallbackExt.name, errorCallbackExt.extension);
66+
6067
Object.assign(window, { htmx });
6168

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

pedagogy/static/pedagogy/css/pedagogy.scss

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,6 @@ $pedagogy-white-text: #f0f0f0;
245245
.input-stars {
246246
margin-top: 20px;
247247
}
248-
249-
.right {
250-
display: flex;
251-
justify-content: flex-end;
252-
}
253-
254248
}
255249

256250
.ue-details-container {
@@ -523,4 +517,9 @@ details.accordion>.accordion-content {
523517
background-color: $white-color;
524518
border-color: $pedagogy-orange;
525519
border-right: none;
520+
}
521+
522+
.right {
523+
display: flex;
524+
justify-content: flex-end;
526525
}
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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% from "pedagogy/macros.jinja" import display_star %}
2-
{% from "core/macros.jinja" import user_profile_link %}
2+
{% from "core/macros.jinja" import user_profile_link, commentUpdate %}
33

44
<div id="comment-{{ comment.id }}" class="comment-container">
55

@@ -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="{{ 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<span class="grade-text"> {% trans %} not rated {% endtrans %} </span>
1414
{% endif %}
1515

16-
{%- endmacro %}
16+
{%- endmacro %}
17+
18+
{% macro commentUpdate %}
19+
document.getElementsByTagName('body')[0].dispatchEvent(new CustomEvent('CommentUpdate'))
20+
{% 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)