Compare commits

..
16 Commits
Author SHA1 Message Date
imperosol 622252d528 ts-ify guide-index.ts 2026-09-04 16:12:57 +02:00
thomas girod d2c259c4f9 Merge pull request #1474 from ae-utbm/404-msg
funny 404 message
2026-09-04 16:12:37 +02:00
thomas girod f57fe290d6 Merge pull request #1475 from ae-utbm/fix-timetable-regex
fix: timetable
2026-09-04 15:55:37 +02:00
imperosol e7b5330f4f make timetable generator available to everyone 2026-09-04 15:50:21 +02:00
imperosol 3cdbb9605c fix: timetable regex 2026-09-04 15:47:46 +02:00
imperosol c17f2feefc funny 404 message 2026-09-04 11:25:06 +02:00
thomas girod 22ecd01898 Merge pull request #1473 from ae-utbm/js-markdown
fix: use good version of aemark
2026-09-03 16:31:48 +02:00
imperosol 1fa4ccd710 fix: use good version of aemark 2026-09-03 16:30:05 +02:00
thomas girod 2a71708ccd Merge pull request #1472 from ae-utbm/js-markdown
Js markdown
2026-09-03 16:22:43 +02:00
imperosol 549d8c67e7 doc: ae markdown 2026-09-03 16:10:18 +02:00
imperosol ff69741c82 remove /api/markdown route 2026-09-03 12:03:52 +02:00
imperosol b5b837498f Use client-side md parser for easymde 2026-09-03 11:50:55 +02:00
imperosol 8ef3054888 add @ae_utbm/aemark to JS deps 2026-09-03 11:50:55 +02:00
thomas girod 362f1a6a62 Merge pull request #1470 from ae-utbm/subscription-creation-date
add created_at column to Subscription
2026-09-01 23:35:33 +02:00
imperosol b6347829c6 add translations 2026-09-01 18:34:03 +02:00
imperosol 2099e1bd36 add created_at column to Subscription 2026-09-01 15:45:51 +02:00
29 changed files with 447 additions and 530 deletions
-11
View File
@@ -3,7 +3,6 @@ from typing import Annotated, Literal
from annotated_types import Ge, Le, MinLen from annotated_types import Ge, Le, MinLen
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from django.http import HttpResponse
from ninja import File, Query from ninja import File, Query
from ninja.security import SessionAuth from ninja.security import SessionAuth
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
@@ -18,7 +17,6 @@ from core.models import Group, QuickUploadImage, SithFile, User
from core.schemas import ( from core.schemas import (
FamilyGodfatherSchema, FamilyGodfatherSchema,
GroupSchema, GroupSchema,
MarkdownSchema,
SithFileSchema, SithFileSchema,
UploadedFileSchema, UploadedFileSchema,
UploadedImage, UploadedImage,
@@ -28,18 +26,9 @@ from core.schemas import (
UserSchema, UserSchema,
ValidationErrorSchema, ValidationErrorSchema,
) )
from core.templatetags.renderer import markdown
from counter.utils import is_logged_in_counter from counter.utils import is_logged_in_counter
@api_controller("/markdown")
class MarkdownController(ControllerBase):
@route.post("", url_name="markdown")
def render_markdown(self, body: MarkdownSchema):
"""Convert the markdown text into html."""
return HttpResponse(markdown(body.text), content_type="text/html")
@api_controller("/upload") @api_controller("/upload")
class UploadController(ControllerBase): class UploadController(ControllerBase):
@route.post( @route.post(
-4
View File
@@ -161,10 +161,6 @@ class UserFilterSchema(FilterSchema):
return value return value
class MarkdownSchema(Schema):
text: str
class FamilyGodfatherSchema(Schema): class FamilyGodfatherSchema(Schema):
godfather: int godfather: int
godchild: int godchild: int
+13 -7
View File
@@ -7,7 +7,7 @@
*/ */
// Must be loaded before Apline // Must be loaded before Apline
import htmx, { HtmxResponse } from "htmx.org"; import htmx 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";
@@ -48,13 +48,19 @@ polyfillCountryFlagEmojis();
/** /**
* HTMX * HTMX
*/ */
document.body.addEventListener("htmx:before:request", (event) => { document.body.addEventListener(
event.target.ariaBusy = true; "htmx:before:request" as keyof HTMLElementEventMap,
}); (event) => {
(event as CustomEvent).detail.ctx.target.ariaBusy = true;
},
);
document.body.addEventListener("htmx:before:swap", (event) => { document.body.addEventListener(
event.target.ariaBusy = null; "htmx:before:swap" as keyof HTMLElementEventMap,
}); (event) => {
(event as CustomEvent).detail.ctx.target.ariaBusy = null;
},
);
Object.assign(window, { htmx }); Object.assign(window, { htmx });
@@ -3,16 +3,12 @@
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
// @ts-expect-error 2307 // @ts-expect-error 2307
import "easymde/src/css/easymde.css"; import "easymde/src/css/easymde.css";
import { markdown } from "@ae_utbm/aemark";
// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE // biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE
import type CodeMirror from "codemirror"; import type CodeMirror from "codemirror"; // biome-ignore lint/style/useNamingConvention: This is how they called their namespace
// biome-ignore lint/style/useNamingConvention: This is how they called their namespace
import EasyMDE from "easymde"; import EasyMDE from "easymde";
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
import { import { type UploadUploadImageErrors, uploadUploadImage } from "#openapi";
markdownRenderMarkdown,
type UploadUploadImageErrors,
uploadUploadImage,
} from "#openapi";
const loadEasyMde = (textarea: HTMLTextAreaElement) => { const loadEasyMde = (textarea: HTMLTextAreaElement) => {
const easymde = new EasyMDE({ const easymde = new EasyMDE({
@@ -64,19 +60,7 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
}); });
easymde.codemirror.replaceSelection("\n"); easymde.codemirror.replaceSelection("\n");
}, },
previewRender: (plainText, preview) => { previewRender: (plainText) => markdown(plainText),
/* This is wrapped this way to allow time for Alpine to be loaded on the page */
return Alpine.debounce(() => {
const func = async () => {
preview.innerHTML = (
await markdownRenderMarkdown({ body: { text: plainText } })
).data as string;
return null;
};
func().then();
return null;
}, 300)();
},
forceSync: true, // Avoid validation error on generic create view forceSync: true, // Avoid validation error on generic create view
imageTexts: { imageTexts: {
sbInit: gettext("Attach files by drag and dropping or pasting from clipboard."), sbInit: gettext("Attach files by drag and dropping or pasting from clipboard."),
+4
View File
@@ -4,6 +4,10 @@
<div id="page"> <div id="page">
<h3>{% trans %}404, Not Found{% endtrans %}</h3> <h3>{% trans %}404, Not Found{% endtrans %}</h3>
<blockquote>
{% trans %}Impossible, perhaps the archives are incomplete{% endtrans %}
</blockquote>
</div> </div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -473,7 +473,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView): class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView):
model = User model = User
form_class = UserVisibilityForm form_class = UserVisibilityForm
template_name = "core/fragments/user_visibility.jinja" template_name = "core/fragment/user_visibility.jinja"
pk_url_kwarg = "user_id" pk_url_kwarg = "user_id"
def get_form_kwargs(self): def get_form_kwargs(self):
+51
View File
@@ -0,0 +1,51 @@
## syntaxe aemark
Le site AE utilise markdown pour le rendu de la plupart
des textes saisis par les utilisateurs.
Cependant, la syntaxe utilisée n'est pas celle officielle
telle que définie par John Gruber,
mais est basée sur [CommonMark](https://commonmark.org/),
avec quelques variations pour les usages particuliers du site AE.
Les deux principaux ajouts sont :
- Les urls commençant par `page://` sont modifiées pour commencer par `/page/`
- Des modificateurs de taille peuvent être indiqués directement dans la source
d'une image
Les variations d'aemark sont documentées sur
[le site AE](https://ae.utbm.fr/page/Aide_sur_la_syntaxe/).
Le code est hébergé sur le dépôt Git [aemark](https://github.com/ae-utbm/ae-markdown).
## Utiliser aemark
Le code du parser aemark est écrit en Rust dans une librairie
indépendante, avec des bindings vers différents langages.
Les librairies mises à disposition exposent une seule fonction,
qui prend simplement du markdown en entrée et renvoie l'HTML correspondant.
Les librairies pour les différents langages sont toutes
basées sur le même code Rust.
La seule différence entre chacune tient uniquement dans la manière
d'intégrer ce code dans les bindings.
De cette manière, le comportement est assuré d'être le même
sur toutes les plateformes disponibles, avec en prime des performances
respectables.
=== ":simple-python: Python"
```python
from aemark import markdown
result = markdown("this is some *markdown text* with __formatting__")
```
=== ":simple-javascript: Javascript"
```typescript
import { markdown } from "@ae_utbm/aemark"
const result = markdown("this is some *markdown text* with __formatting__");
```
+21 -1
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-21 14:10+0200\n" "POT-Creation-Date: 2026-09-04 11:24+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -2049,6 +2049,10 @@ msgstr "403, Non autorisé"
msgid "404, Not Found" msgid "404, Not Found"
msgstr "404. Non trouvé" msgstr "404. Non trouvé"
#: core/templates/core/404.jinja
msgid "Impossible, perhaps the archives are incomplete"
msgstr "Impossible, les archives sont peut-être incomplètes"
#: core/templates/core/500.jinja #: core/templates/core/500.jinja
msgid "500, Server Error" msgid "500, Server Error"
msgstr "500, Erreur Serveur" msgstr "500, Erreur Serveur"
@@ -5089,6 +5093,10 @@ msgstr "signalant"
msgid "A guide of courses available at UTBM." msgid "A guide of courses available at UTBM."
msgstr "Un guide de tous les cours disponibles à l'UTBM." msgstr "Un guide de tous les cours disponibles à l'UTBM."
#: pedagogy/templates/pedagogy/guide.jinja
msgid "Search UE"
msgstr "Recherche d'UE"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
#, python-format #, python-format
msgid "%(display_name)s" msgid "%(display_name)s"
@@ -5758,6 +5766,18 @@ msgstr "fin de la cotisation"
msgid "location" msgid "location"
msgstr "lieu" msgstr "lieu"
#: subscription/models.py
msgid "created_at"
msgstr "créé le"
#: subscription/models.py
msgid ""
"When this subscription was created. This date may differ from the start of "
"the subscription."
msgstr ""
"Quand la cotisation a été créée. Cette date peut différer du début effectif "
"de la cotisation."
#: subscription/models.py #: subscription/models.py
msgid "You can not subscribe many time for the same period" msgid "You can not subscribe many time for the same period"
msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période"
+1
View File
@@ -71,6 +71,7 @@ nav:
- API: - API:
- Développement: tutorial/api/dev.md - Développement: tutorial/api/dev.md
- Connexion à l'API: tutorial/api/connect.md - Connexion à l'API: tutorial/api/connect.md
- Markdown AE: tutorial/markdown.md
- Etransactions: tutorial/etransaction.md - Etransactions: tutorial/etransaction.md
- How-to: - How-to:
- L'ORM de Django: howto/querysets.md - L'ORM de Django: howto/querysets.md
+6
View File
@@ -9,6 +9,7 @@
"version": "3", "version": "3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@ae_utbm/aemark": "^0.1.3",
"@alpinejs/sort": "^3.16.2", "@alpinejs/sort": "^3.16.2",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.8.0", "@floating-ui/dom": "^1.8.0",
@@ -56,6 +57,11 @@
"vite": "^8.2.2" "vite": "^8.2.2"
} }
}, },
"node_modules/@ae_utbm/aemark": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@ae_utbm/aemark/-/aemark-0.1.3.tgz",
"integrity": "sha512-JHb9XDgYxy9J0f7bUWtS7eVptKbeB/4+VQoHO3g1ASwNHZpIpW+RRdJPXA+oi2/RDrdPbEtCMkkVROc+8ifFYQ=="
},
"node_modules/@alpinejs/sort": { "node_modules/@alpinejs/sort": {
"version": "3.16.2", "version": "3.16.2",
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.16.2.tgz", "resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.16.2.tgz",
+1
View File
@@ -40,6 +40,7 @@
"vite": "^8.2.2" "vite": "^8.2.2"
}, },
"dependencies": { "dependencies": {
"@ae_utbm/aemark": "^0.1.3",
"@alpinejs/sort": "^3.16.2", "@alpinejs/sort": "^3.16.2",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.8.0", "@floating-ui/dom": "^1.8.0",
+1 -5
View File
@@ -126,11 +126,7 @@ class UE(models.Model):
Returns: Returns:
True if the user has already posted a comment on this UE, else False. True if the user has already posted a comment on this UE, else False.
""" """
self._has_user_commented = getattr(self, "_has_user_commented", {}) return self.comments.filter(author=user).exists()
self._has_user_commented[user] = self._has_user_commented.get(
user, self.comments.filter(author=user).exists()
)
return self._has_user_commented[user]
@cached_property @cached_property
def grade_global_average(self): def grade_global_average(self):
@@ -1,9 +1,5 @@
import { import { getCurrentUrlParams, updateQueryString } from "#core:utils/history";
getCurrentUrlParams, import { type SimpleUeSchema, ueFetchUeList } from "#openapi";
History,
updateQueryString,
} from "#core:utils/history.ts";
import { ueFetchUeList } from "#openapi";
const pageDefault = 1; const pageDefault = 1;
const pageSizeDefault = 100; const pageSizeDefault = 100;
@@ -12,38 +8,42 @@ document.addEventListener("alpine:init", () => {
Alpine.data("ue_search", () => ({ Alpine.data("ue_search", () => ({
ues: { ues: {
count: 0, count: 0,
next: null, next: null as string | null,
previous: null, previous: null as string | null,
results: [], results: [] as SimpleUeSchema[],
}, },
loading: false, loading: false,
page: pageDefault, page: pageDefault,
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: pageSizeDefault, page_size: pageSizeDefault,
search: "", search: "",
department: [], department: [] as string[],
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: [], credit_type: [] as string[],
semester: [], semester: [] as string[],
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
to_change: [], to_change: [] as { param: string; value: string }[],
pushstate: History.Push,
update: undefined, // dummy implementation to make TS happy.
// The real function is initialized in init
update: () => {
console.warn("Update not yet initialized");
},
initializeArgs() { initializeArgs() {
const url = getCurrentUrlParams(); const url = getCurrentUrlParams();
this.pushstate = History.Replace; this.page = Number.parseInt(url.get("page") || pageDefault.toString(), 10);
this.page_size = Number.parseInt(
this.page = Number.parseInt(url.get("page"), 10) || pageDefault; url.get("page_size") || pageSizeDefault.toString(),
this.page_size = Number.parseInt(url.get("page_size"), 10) || pageSizeDefault; 10,
);
this.search = url.get("search") || ""; this.search = url.get("search") || "";
this.department = url.getAll("department"); this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type"); this.credit_type = url.getAll("credit_type");
/* The semester is easier to use on the backend as an enum (spring/autumn/both/none) /* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
and easier to use on the frontend as an array ([spring, autumn]). and easier to use on the frontend as an array ([spring, autumn]).
Thus there is some conversion involved when both communicate together */ Thus there is some conversion involved when both communicate together */
this.semester = url.has("semester") ? url.get("semester").split("_AND_") : []; this.semester = url.get("semester")?.split("_AND_") || [];
this.update(); this.update();
}, },
@@ -51,15 +51,11 @@ document.addEventListener("alpine:init", () => {
async init() { async init() {
this.update = Alpine.debounce(async () => { this.update = Alpine.debounce(async () => {
/* Create the whole url before changing everything all at once */ /* Create the whole url before changing everything all at once */
const first = this.to_change.shift(); for (const val of this.to_change) {
let url = updateQueryString(first.param, first.value, History.None); updateQueryString(val.param, val.value);
for (const value of this.to_change) {
url = updateQueryString(value.param, value.value, History.None, url);
} }
updateQueryString(first.param, first.value, this.pushstate, url);
await this.fetchData(); /* reload data on form change */ await this.fetchData(); /* reload data on form change */
this.to_change = []; this.to_change = [];
this.pushstate = History.Push;
}, 50); }, 50);
const searchParams = ["search", "department", "credit_type", "semester"]; const searchParams = ["search", "department", "credit_type", "semester"];
@@ -67,47 +63,37 @@ document.addEventListener("alpine:init", () => {
for (const param of searchParams) { for (const param of searchParams) {
this.$watch(param, () => { this.$watch(param, () => {
if (this.pushstate !== History.Push) {
/* This means that we are doing a mass param edit */
return;
}
/* Reset pagination on search */ /* Reset pagination on search */
this.page = pageDefault; this.page = pageDefault;
this.page_size = pageSizeDefault; this.page_size = pageSizeDefault;
}); });
} }
for (const param of searchParams.concat(paginationParams)) { for (const param of searchParams.concat(paginationParams)) {
this.$watch(param, (value) => { this.$watch(param, (value: string) => {
this.to_change.push({ param: param, value: value }); this.to_change.push({ param: param, value: value });
this.update(); this.update();
}); });
} }
window.addEventListener("popstate", () => {
this.initializeArgs();
});
this.initializeArgs(); this.initializeArgs();
}, },
async fetchData() { async fetchData() {
this.loading = true; this.loading = true;
const args = {
const res = await ueFetchUeList({
query: {
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: this.page_size, page_size: this.page_size,
}; // biome-ignore lint/style/useNamingConvention: api is in snake_case
for (const [param, value] of new URL( credit_type: this.credit_type.length > 0 ? this.credit_type : undefined,
window.location.href, semester: this.semester.length > 0 ? this.semester : undefined,
).searchParams.entries()) { department: this.department.length > 0 ? this.department : undefined,
// Deal with array type params search: this.search || undefined,
if (["credit_type", "department", "semester"].includes(param)) { },
if (args[param] === undefined) { });
args[param] = []; if (res.data !== undefined) {
this.ues = res.data;
} }
args[param].push(value);
} else {
args[param] = value;
}
}
this.ues = (await ueFetchUeList({ query: args })).data;
this.loading = false; this.loading = false;
}, },
+7 -9
View File
@@ -128,14 +128,14 @@ $pedagogy-white-text: #f0f0f0;
grid-area: hours-the; grid-area: hours-the;
} }
.leave-comment-not-allowed { #leave_comment_not_allowed {
p { p {
text-align: center; text-align: center;
color: red; color: red;
} }
} }
.leave-comment { #leave_comment {
.leave-comment-grid-container { .leave-comment-grid-container {
display: grid; display: grid;
grid-template-columns: 270px auto; grid-template-columns: 270px auto;
@@ -168,6 +168,10 @@ $pedagogy-white-text: #f0f0f0;
.input-stars { .input-stars {
margin-top: 20px; margin-top: 20px;
} }
input[type="submit"] {
float: right;
}
} }
.ue-details-container { .ue-details-container {
@@ -329,9 +333,8 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px; padding-left: 10px;
} }
.action { .actions {
float: right; float: right;
margin-top: 0;
} }
} }
@@ -441,8 +444,3 @@ details.accordion>.accordion-content {
border-color: $pedagogy-orange; border-color: $pedagogy-orange;
border-right: none; border-right: none;
} }
.right {
display: flex;
justify-content: flex-end;
}
@@ -1,30 +0,0 @@
<form
hx-post="{{ request.get_full_path() }}"
hx-ext="error-callbacks"
hx-target="this"
hx-swap="outerHTML"
hx-disabled-elt="input[type='submit']"
hx-trigger="submit"
hx-callback-404="target.remove()"
>
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.reason.errors }}
{{ form.reason }}
{# Hidden fields #}
{{ form.reporter }}
{{ form.comment }}
<button
hx-get="{{ url('pedagogy:comment_detail', comment_id=comment_id) }}"
hx-target="closest form"
hx-swap="outerHTML"
>
{% trans %}Cancel{% endtrans %}
</button>
<p class="right" id="nique">
<input type="submit" value="{% trans %}Report{% endtrans %}" />
</p>
</form>
@@ -1,83 +0,0 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
<div id="comment-{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<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(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#comment-{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<button
class="btn btn-orange action"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
<i class="fa fa-pencil"></i> {% trans %}Edit{% endtrans %}
</button>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<form class="action"
hx-ext="error-callbacks"
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
hx-callback-404="document.getElementsByTagName('body')[0].dispatchEvent(new CustomEvent('CommentUpdate'));target.remove()"
>
{% csrf_token %}
<button class="btn btn-red action">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</form>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
@@ -1,71 +0,0 @@
<div class="leave-comment">
{% if form.is_creation %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{% endif %}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
{% endif %}
<form
hx-post="{{ action }}"
hx-target="closest .leave-comment"
hx-swap="outerHTML"
hx-disabled-elt="find input[type='submit']"
>
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p class="right">
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
</p>
</form>
{% if form.is_creation %}
</div>
</details>
{% endif %}
<br>
</div>
@@ -1,10 +0,0 @@
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{% for comment in comments %}
{% include "pedagogy/fragments/ue_comment.jinja" %}
{% endfor %}
</section>
@@ -1,10 +0,0 @@
{% 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 %}
@@ -1,27 +0,0 @@
<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>
+159 -32
View File
@@ -1,32 +1,16 @@
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %} {% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %} {% block additional_css %}
<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 %}
{% extends "core/base.jinja" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}"> <link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% trans %}UE Details{% endtrans %} {% trans %}UE Details{% endtrans %}
{% endblock %} {% endblock %}
{% block content %}
{% block content %}
<div class="pedagogy"> <div class="pedagogy">
<div id="ue_detail"> <div id="ue_detail">
<button onclick='(function(){ <button onclick='(function(){
@@ -71,23 +55,166 @@
<br> <br>
<div id="ue-grade"> <div class="ue-details-container">
{% include "pedagogy/fragments/ue_details/grade.jinja" %} <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> </div>
<br> <br>
{% if object.has_user_already_commented(user) %}
<div id="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>
{% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{%endif%}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
<form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
<div id="comment-form"> {{ form.author }}
{% include "pedagogy/fragments/ue_details/form.jinja" %} {{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div> </div>
<div id="comments"> <div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p><input type="submit" value="{% trans %}Comment{% endtrans %}" /></p>
</form>
</div>
</details>
{% endif %}
<br>
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
{% for comment in comments %} {% for comment in comments %}
{% include "pedagogy/fragments/ue_comment.jinja" %} <div id="{{ comment.id }}" class="comment-container">
{% endfor %}
<div class="grade-block">
<div class="grade-type">
<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(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<p class="actions">
<a href="{{ url('pedagogy:comment_update', comment_id=comment.id) }}">
{% trans %}Edit{% endtrans %}
</a>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<a href="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</p>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div> </div>
</div> </div>
{% endfor %}
{% endif %}
</div> </div>
{% endblock %} </div>
{% endif %} {% endblock %}
+2 -14
View File
@@ -24,14 +24,12 @@
from django.urls import path from django.urls import path
from pedagogy.views import ( from pedagogy.views import (
UECommentCreateView,
UECommentDeleteView, UECommentDeleteView,
UECommentDetailView,
UECommentReportCreateView, UECommentReportCreateView,
UECommentUpdateView, UECommentUpdateView,
UECreateView, UECreateView,
UEDeleteView, UEDeleteView,
UEDetailView, UEDetailFormView,
UEGuideView, UEGuideView,
UEModerationFormView, UEModerationFormView,
UEUpdateView, UEUpdateView,
@@ -40,17 +38,7 @@ from pedagogy.views import (
urlpatterns = [ 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>/", UEDetailFormView.as_view(), name="ue_detail"),
path(
"ue/<int:ue_id>/comment",
UECommentCreateView.as_view(),
name="comment_create",
),
path(
"comment/<int:comment_id>/",
UECommentDetailView.as_view(),
name="comment_detail",
),
path( path(
"comment/<int:comment_id>/edit/", "comment/<int:comment_id>/edit/",
UECommentUpdateView.as_view(), UECommentUpdateView.as_view(),
+35 -89
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 Software Foundation, Inc., 59 Temple # this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
@@ -29,7 +29,6 @@ from django.urls import reverse, reverse_lazy
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView,
FormView, FormView,
TemplateView, TemplateView,
UpdateView, UpdateView,
@@ -37,7 +36,7 @@ from django.views.generic import (
from core.auth.mixins import PermissionOrAuthorRequiredMixin from core.auth.mixins import PermissionOrAuthorRequiredMixin
from core.models import Notification, User from core.models import Notification, User
from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin from core.views import DetailFormView
from pedagogy.forms import ( from pedagogy.forms import (
UECommentForm, UECommentForm,
UECommentModerationForm, UECommentModerationForm,
@@ -47,59 +46,37 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView): class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
model = UEComment """Display every comment of an UE and detailed infos about it.
template_name = "pedagogy/fragments/ue_comment_form.jinja"
form_class = UECommentForm
permission_required = "pedagogy.add_uecomment"
object = None # Avoid initialisation bug with FragmentMixin
@property Allow to comment the UE.
def ue(self): """
if hasattr(self, "_ue"):
return self._ue model = UE
self._ue = get_object_or_404(UE, id=self.kwargs.get("ue_id")) pk_url_kwarg = "ue_id"
return self._ue template_name = "pedagogy/ue_detail.jinja"
form_class = UECommentForm
permission_required = "pedagogy.view_ue"
def has_permission(self): def has_permission(self):
if self.ue.has_user_already_commented(self.request.user): if self.request.method == "POST" and not self.request.user.has_perm(
"pedagogy.add_uecomment"
):
# if it's a POST request, the user is trying to add a new UEComment
# thus he also needs the "add_uecomment" permission
return False return False
return super().has_permission() return super().has_permission()
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.request.user.id kwargs["author_id"] = self.request.user.id
kwargs["ue_id"] = self.ue.id kwargs["ue_id"] = self.object.id
kwargs["is_creation"] = True kwargs["is_creation"] = True
return kwargs return kwargs
def get_context_data(self, **kwargs): def form_valid(self, form):
return super().get_context_data(**kwargs) | { form.save()
"action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}), return super().form_valid(form)
"object": self.ue,
}
def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue.id})
class UEDetailView(
PermissionRequiredMixin, UseFragmentsMixin, AllowFragment, DetailView
):
"""Display every comment of an UE and detailed infos about it."""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
permission_required = "pedagogy.view_ue"
fragments = {
"add_comment_form": UECommentCreateView,
}
def get_fragment_data(self):
return {
"add_comment_form": {"ue_id": self.object.id},
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
@@ -108,35 +85,22 @@ class UEDetailView(
.annotate_is_reported() .annotate_is_reported()
.select_related("author") .select_related("author")
.order_by("-publish_date") .order_by("-publish_date")
),
}
class UECommentDetailView(PermissionRequiredMixin, DetailView):
model = UEComment
pk_url_kwarg = "comment_id"
template_name = "pedagogy/fragments/ue_comment.jinja"
permission_required = "pedagogy.view_ue"
context_object_name = "comment"
def get_queryset(self):
return (
super().get_queryset().viewable_by(self.request.user).annotate_is_reported()
) )
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"ue": getattr(self.object, "ue", None)
} }
def get_success_url(self):
# once the new ue comment has been saved
# redirect to the same page we are currently
return self.request.path
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
"""Allow edit of a given comment.""" """Allow edit of a given comment."""
model = UEComment model = UEComment
form_class = UECommentForm form_class = UECommentForm
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
template_name = "pedagogy/fragments/ue_comment_form.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.change_uecomment" permission_required = "pedagogy.change_uecomment"
author_field = "author" author_field = "author"
@@ -147,18 +111,11 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, Update
kwargs["is_creation"] = False kwargs["is_creation"] = False
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse(
"pedagogy:comment_update", kwargs={"comment_id": self.object.id}
)
}
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:comment_detail", kwargs={"comment_id": self.object.id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, DeleteView): class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
"""Allow to delete a given comment.""" """Allow to delete a given comment."""
model = UEComment model = UEComment
@@ -167,13 +124,8 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, Delete
permission_required = "pedagogy.delete_uecomment" permission_required = "pedagogy.delete_uecomment"
author_field = "author" author_field = "author"
def form_valid(self, form):
response = super().form_valid(form)
response.headers["HX-Trigger"] = "CommentUpdate"
return response
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:comment_detail", kwargs={"comment_id": self.object.id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UEGuideView(PermissionRequiredMixin, TemplateView): class UEGuideView(PermissionRequiredMixin, TemplateView):
@@ -183,12 +135,12 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView): class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
"""Create a new report for an inappropriate comment.""" """Create a new report for an inappropriate comment."""
model = UECommentReport model = UECommentReport
form_class = UECommentReportForm form_class = UECommentReportForm
template_name = "pedagogy/fragments/comment_report.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.add_uecommentreport" permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -201,11 +153,6 @@ class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateVi
kwargs["comment_id"] = self.ue_comment.id kwargs["comment_id"] = self.ue_comment.id
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data() | {
"comment_id": self.ue_comment.id,
}
def form_valid(self, form): def form_valid(self, form):
resp = super().form_valid(form) resp = super().form_valid(form)
# Send a message to moderation admins # Send a message to moderation admins
@@ -221,12 +168,11 @@ class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateVi
url=reverse("pedagogy:moderation"), url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION", type="PEDAGOGY_MODERATION",
) )
return resp return resp
def get_success_url(self): def get_success_url(self):
return reverse( return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id})
"pedagogy:comment_detail", kwargs={"comment_id": self.ue_comment.id}
)
class UEModerationFormView(PermissionRequiredMixin, FormView): class UEModerationFormView(PermissionRequiredMixin, FormView):
@@ -0,0 +1,43 @@
# Generated by Django 5.2.17 on 2026-09-01 08:44
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F, Value
from django.db.models.functions import Least
from django.utils.timezone import now
def make_default_creation_date(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
Subscription.objects.update(
created_at=Least(
F("subscription_start"), Value(now()), output_field=models.DateTimeField()
)
)
class Migration(migrations.Migration):
dependencies = [("subscription", "0016_alter_subscription_subscription_type")]
operations = [
migrations.AddField(
model_name="subscription",
name="created_at",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
help_text=(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
verbose_name="created_at",
),
preserve_default=False,
),
migrations.RunPython(
make_default_creation_date,
reverse_code=migrations.RunPython.noop,
elidable=True,
),
]
+8
View File
@@ -64,6 +64,14 @@ class Subscription(models.Model):
max_length=20, max_length=20,
verbose_name=_("location"), verbose_name=_("location"),
) )
created_at = models.DateTimeField(
_("created_at"),
help_text=_(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
auto_now_add=True,
)
class Meta: class Meta:
ordering = ["subscription_start"] ordering = ["subscription_start"]
@@ -1,8 +1,8 @@
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
// see https://regex101.com/r/QHSaPM/2 // see https://regex101.com/r/QHSaPM/3
const TIMETABLE_ROW_RE: RegExp = const TIMETABLE_ROW_RE: RegExp =
/^(?<ueCode>\w.+\w)\s+(?<courseType>[A-Z]{2}\d)\s+((?<weekGroup>[AB])\s+)?(?<weekday>(lundi)|(mardi)|(mercredi)|(jeudi)|(vendredi)|(samedi)|(dimanche))\s+(?<startHour>\d{2}:\d{2})\s+(?<endHour>\d{2}:\d{2})\s+[\dA-B]\s+((?<attendance>[\wé]*)\s+)?(?<room>\w+(?:, \w+)?)$/; /^(?<ueCode>\w.+\w)\s+(?<courseType>[A-Z]{2}\d)\s+((?<weekGroup>[AB])\s+)?(?<weekday>(lundi)|(mardi)|(mercredi)|(jeudi)|(vendredi)|(samedi)|(dimanche))\s+(?<startHour>\d{2}:\d{2})\s+(?<endHour>\d{2}:\d{2})\s+[\dA-B]\s+((?<attendance>[\wé]*)\s+)?(?<room>\w+(?:, \w+)*)$/;
const DEFAULT_TIMETABLE: string = `DS52\t\tCM1\t\tlundi\t08:00\t10:00\t1\tPrésentiel\tA113 const DEFAULT_TIMETABLE: string = `DS52\t\tCM1\t\tlundi\t08:00\t10:00\t1\tPrésentiel\tA113
DS53\t\tCM1\t\tlundi\t10:15\t12:15\t1\tPrésentiel\tA101 DS53\t\tCM1\t\tlundi\t10:15\t12:15\t1\tPrésentiel\tA101
+1 -3
View File
@@ -1,8 +1,6 @@
# Create your views here. # Create your views here.
from django.views.generic import TemplateView from django.views.generic import TemplateView
from core.auth.mixins import FormerSubscriberMixin
class GeneratorView(TemplateView):
class GeneratorView(FormerSubscriberMixin, TemplateView):
template_name = "timetable/generator.jinja" template_name = "timetable/generator.jinja"