From 22b510e25bc3375491eaba09a45843e40144a57c Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 9 Oct 2026 16:09:54 +0200 Subject: [PATCH 01/35] short-circuit `ForumMessageView` redirection on 403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand on accède à cette vue, on est immédiatement redirigé vers le topic correspondant (quitte à se faire 403 une fois arrivé là). Comme la redirection est une opération qui a un cout, on peut gagner du temps en mettant un PermissionDenied directement (d'autant plus que les bots aiment beaucoup rechercher les messages du forum, et qu'ils se font systématiquement 403) --- forum/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/forum/views.py b/forum/views.py index 3bd786d9..ae5e3750 100644 --- a/forum/views.py +++ b/forum/views.py @@ -346,13 +346,16 @@ class ForumTopicDetailView(CanViewMixin, DetailView): return kwargs -class ForumMessageView(SingleObjectMixin, RedirectView): +class ForumMessageView(SingleObjectMixin, UserPassesTestMixin, RedirectView): model = ForumMessage pk_url_kwarg = "message_id" permanent = False - def get_redirect_url(self, *args, **kwargs): + def test_func(self) -> bool: self.object = self.get_object() + return self.request.user.can_view(self.object) + + def get_redirect_url(self, *args, **kwargs): return self.object.get_url() From 0c6594b3cde4e4d1a31bf31d1a210237614b112f Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 9 Oct 2026 21:59:37 +0200 Subject: [PATCH 02/35] remove `CounterAdminMixin` --- counter/templates/counter/product_form.jinja | 40 ++++-- counter/templates/counter/product_list.jinja | 8 +- .../templates/counter/product_type_list.jinja | 6 +- counter/views/admin.py | 55 ++++--- counter/views/eticket.py | 13 +- counter/views/mixins.py | 134 +++++++++--------- locale/fr/LC_MESSAGES/django.po | 24 ++-- 7 files changed, 154 insertions(+), 126 deletions(-) diff --git a/counter/templates/counter/product_form.jinja b/counter/templates/counter/product_form.jinja index be1fd850..c30b3e86 100644 --- a/counter/templates/counter/product_form.jinja +++ b/counter/templates/counter/product_form.jinja @@ -83,8 +83,17 @@ {% endmacro %} {% block content %} +
+
+ {%- trans trimmed -%} + You can see product details. + However, you cannot edit it, thus you won't be able + to submit this form. + {%- endtrans -%} +
+
{% if object %} -

{% trans name=object %}Edit product {{ name }}{% endtrans %}

+

{{ object }}

{% trans %}Creation date{% endtrans %} : {{ object.created_at|date }}

{% trans %}Last update{% endtrans %} : {{ object.updated_at|date }}

{% else %} @@ -127,19 +136,17 @@ {{ form.price_formset.management_form }}
{%- for form in form.price_formset.forms -%} -
- {{ price_form(form) }} -
+
{{ price_form(form) }}
{%- endfor -%}
- + {% if user.has_perm("counter.change_product") %} + + {% endif %}
@@ -165,11 +172,16 @@ - + {% if user.has_perm("counter.change_product") %} + + {% endif %}
{{ form.archived.as_field_group() }}
-

+ + {% if user.has_perm("counter.change_product") %} +

+ {% endif %} {% endblock %} diff --git a/counter/templates/counter/product_list.jinja b/counter/templates/counter/product_list.jinja index ca2e4c4d..b5c3c7fa 100644 --- a/counter/templates/counter/product_list.jinja +++ b/counter/templates/counter/product_list.jinja @@ -80,9 +80,11 @@

{% trans %}Product list{% endtrans %}

- - {% trans %}New product{% endtrans %} - + {% if user.has_perm("counter.add_product") %} + + {% trans %}New product{% endtrans %} + + {% endif %}
- + {% endblock content %} From ed888834d65a6f54c4b293b3e9b80a9ec957400a Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 3 Oct 2026 23:21:32 +0200 Subject: [PATCH 14/35] split timetable parsing and display --- ...{generator-index.ts => timetable-index.ts} | 85 +++++-------------- timetable/static/timetable/css/generator.scss | 2 +- timetable/templates/timetable/generator.jinja | 43 ++-------- timetable/templates/timetable/macros.jinja | 46 ++++++++++ 4 files changed, 76 insertions(+), 100 deletions(-) rename timetable/static/bundled/timetable/{generator-index.ts => timetable-index.ts} (77%) create mode 100644 timetable/templates/timetable/macros.jinja diff --git a/timetable/static/bundled/timetable/generator-index.ts b/timetable/static/bundled/timetable/timetable-index.ts similarity index 77% rename from timetable/static/bundled/timetable/generator-index.ts rename to timetable/static/bundled/timetable/timetable-index.ts index 3ead147a..a121a48a 100644 --- a/timetable/static/bundled/timetable/generator-index.ts +++ b/timetable/static/bundled/timetable/timetable-index.ts @@ -78,7 +78,10 @@ function parseSlots(s: string): TimetableSlot[] { } document.addEventListener("alpine:init", () => { - Alpine.data("timetableGenerator", () => ({ + /** + * Alpine data that can parse a timetable, save it and export it to png. + */ + Alpine.data("timetableGenerator", (userId: number) => ({ content: DEFAULT_TIMETABLE, error: null as { incorrectRow?: string } | null, courses: [] as TimetableSlot[], @@ -111,6 +114,8 @@ document.addEventListener("alpine:init", () => { async saveSith() { this.saving = true; const res = await timetableSaveTimetable({ + // biome-ignore lint/style/useNamingConvention: api is snake case + path: { user_id: userId }, body: { // biome-ignore lint/style/useNamingConvention: api is snake case is_viewable: this.edtPublic, @@ -151,13 +156,10 @@ document.addEventListener("alpine:init", () => { */ Alpine.data("timetableDisplay", (slots: TimetableSlot[]) => ({ displayedWeekdays: [] as WeekDay[], - courses: [] as TimetableSlot[], startSlot: 0, endSlot: 0, table: { height: 0, width: 0 }, - edtPublic: false, - saving: false, - timetableSavedMessage: new AlertMessage(), + slots: slots, colors: {} as Record, colorPalette: [ @@ -174,19 +176,14 @@ document.addEventListener("alpine:init", () => { "#1b5e20", ], - generate() { - try { - this.courses = parseSlots(this.content); - this.error = null; - } catch (err) { - type ParseRowError = { cause: { row: string } }; - this.error = { incorrectRow: (err as ParseRowError)?.cause?.row }; - return; - } + init() { + this.$watch("slots", () => this.display()); + }, + display() { // color each UE let colorIndex = 0; - for (const slot of this.courses) { + for (const slot of this.slots) { if (!this.colors[slot.ueCode]) { this.colors[slot.ueCode] = this.colorPalette[colorIndex % this.colorPalette.length]; @@ -195,13 +192,13 @@ document.addEventListener("alpine:init", () => { } this.displayedWeekdays = WEEKDAYS.filter((day) => - this.courses.some((slot: TimetableSlot) => slot.weekday === day), + this.slots.some((slot: TimetableSlot) => slot.weekday === day), ); - this.startSlot = this.courses.reduce( + this.startSlot = this.slots.reduce( (acc: number, curr: TimetableSlot) => Math.min(acc, curr.startSlot), 25 * 4, ); - this.endSlot = this.courses.reduce( + this.endSlot = this.slots.reduce( (acc: number, curr: TimetableSlot) => Math.max(acc, curr.endSlot), 1, ); @@ -222,15 +219,19 @@ document.addEventListener("alpine:init", () => { }; }, - getHours(): [string, object][] { + /** + * Return the hours that should be displayed on this timetable, + * with their `top` placing + */ + getHours(): [string, { top: `${number}px` }][] { let hour: number = Number.parseInt( - this.courses + this.slots .map((c: TimetableSlot) => c.startHour) .reduce((res: string, hour: string) => (hour < res ? hour : res), "24:00") .split(":")[0], 10, ); - const res: [string, object][] = []; + const res: [string, { top: `${number}px` }][] = []; for (let i = 0; i <= this.endSlot - this.startSlot; i += 60 / MINUTES_PER_SLOT) { res.push([`${hour}:00`, { top: `${i * SLOT_HEIGHT}px` }]); hour += 1; @@ -241,47 +242,5 @@ document.addEventListener("alpine:init", () => { getWidth() { return this.displayedWeekdays.length * SLOT_WIDTH + 20; }, - - async savePng() { - const elem = document.getElementById("timetable"); - const img = (await html2canvas(elem as HTMLElement)).toDataURL(); - const downloadLink = document.createElement("a"); - downloadLink.href = img; - downloadLink.download = "edt.png"; - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - }, - - async saveSith() { - this.saving = true; - const res = await timetableSaveTimetable({ - body: { - // biome-ignore lint/style/useNamingConvention: api is snake case - is_viewable: this.edtPublic, - slots: this.courses.map((c) => - Object.assign({}, c, { - // biome-ignore lint/style/useNamingConvention: api is snake case - start_at: c.startSlot, - // biome-ignore lint/style/useNamingConvention: api is snake case - end_at: c.endSlot, - weekday: WEEKDAYS.indexOf(c.weekday) + 1, - }), - ), - }, - }); - if (res.response?.ok) { - this.timetableSavedMessage.display( - gettext("This timetable has been saved in your profile"), - { success: true }, - ); - } else { - this.timetableSavedMessage.display( - interpolate(gettext("Error %d: timetable save failed"), res.response?.status), - { success: false }, - ); - } - this.saving = false; - }, })); }); diff --git a/timetable/static/timetable/css/generator.scss b/timetable/static/timetable/css/generator.scss index 758892de..095ed30e 100644 --- a/timetable/static/timetable/css/generator.scss +++ b/timetable/static/timetable/css/generator.scss @@ -1,6 +1,6 @@ @import "core/static/core/colors"; -#timetable { +.timetable { --hour-side-width: 60px; display: block; diff --git a/timetable/templates/timetable/generator.jinja b/timetable/templates/timetable/generator.jinja index 0769181f..7c2964f6 100644 --- a/timetable/templates/timetable/generator.jinja +++ b/timetable/templates/timetable/generator.jinja @@ -1,11 +1,13 @@ -{% extends 'core/base.jinja' %} +{% extends "core/base.jinja" %} + +{% from "timetable/macros.jinja" import display_timetable %} {%- block additional_css -%} {%- endblock -%} {%- block additional_js -%} - + {%- endblock -%} {% block title %} @@ -32,7 +34,7 @@ {% endblock %} {% block content %} -
+

{% trans %}Timetable generator{% endtrans %}