mirror of
https://github.com/ae-utbm/sith.git
synced 2026-09-03 11:09:17 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582e6af2c9 | ||
|
|
bbdf6a5dd2 |
+11
@@ -3,6 +3,7 @@ from typing import Annotated, Literal
|
||||
from annotated_types import Ge, Le, MinLen
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.http import HttpResponse
|
||||
from ninja import File, Query
|
||||
from ninja.security import SessionAuth
|
||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||
@@ -17,6 +18,7 @@ from core.models import Group, QuickUploadImage, SithFile, User
|
||||
from core.schemas import (
|
||||
FamilyGodfatherSchema,
|
||||
GroupSchema,
|
||||
MarkdownSchema,
|
||||
SithFileSchema,
|
||||
UploadedFileSchema,
|
||||
UploadedImage,
|
||||
@@ -26,9 +28,18 @@ from core.schemas import (
|
||||
UserSchema,
|
||||
ValidationErrorSchema,
|
||||
)
|
||||
from core.templatetags.renderer import markdown
|
||||
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")
|
||||
class UploadController(ControllerBase):
|
||||
@route.post(
|
||||
|
||||
@@ -161,6 +161,10 @@ class UserFilterSchema(FilterSchema):
|
||||
return value
|
||||
|
||||
|
||||
class MarkdownSchema(Schema):
|
||||
text: str
|
||||
|
||||
|
||||
class FamilyGodfatherSchema(Schema):
|
||||
godfather: int
|
||||
godchild: int
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
import "codemirror/lib/codemirror.css";
|
||||
// @ts-expect-error 2307
|
||||
import "easymde/src/css/easymde.css";
|
||||
import { markdown } from "@ae_utbm/aemark";
|
||||
// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE
|
||||
import type CodeMirror from "codemirror"; // biome-ignore lint/style/useNamingConvention: This is how they called their namespace
|
||||
import type CodeMirror from "codemirror";
|
||||
// biome-ignore lint/style/useNamingConvention: This is how they called their namespace
|
||||
import EasyMDE from "easymde";
|
||||
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
|
||||
import { type UploadUploadImageErrors, uploadUploadImage } from "#openapi";
|
||||
import {
|
||||
markdownRenderMarkdown,
|
||||
type UploadUploadImageErrors,
|
||||
uploadUploadImage,
|
||||
} from "#openapi";
|
||||
|
||||
const loadEasyMde = (textarea: HTMLTextAreaElement) => {
|
||||
const easymde = new EasyMDE({
|
||||
@@ -60,7 +64,19 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
|
||||
});
|
||||
easymde.codemirror.replaceSelection("\n");
|
||||
},
|
||||
previewRender: (plainText) => markdown(plainText),
|
||||
previewRender: (plainText, preview) => {
|
||||
/* 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
|
||||
imageTexts: {
|
||||
sbInit: gettext("Attach files by drag and dropping or pasting from clipboard."),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-01 18:32+0200\n"
|
||||
"POT-Creation-Date: 2026-08-21 14:10+0200\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@@ -5089,10 +5089,6 @@ msgstr "signalant"
|
||||
msgid "A guide of courses available at 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
|
||||
#, python-format
|
||||
msgid "%(display_name)s"
|
||||
@@ -5762,18 +5758,6 @@ msgstr "fin de la cotisation"
|
||||
msgid "location"
|
||||
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
|
||||
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"
|
||||
|
||||
Generated
-6
@@ -9,7 +9,6 @@
|
||||
"version": "3",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@ae_utbm/aemark": "^0.1.1",
|
||||
"@alpinejs/sort": "^3.16.2",
|
||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
@@ -57,11 +56,6 @@
|
||||
"vite": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@ae_utbm/aemark": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ae_utbm/aemark/-/aemark-0.1.1.tgz",
|
||||
"integrity": "sha512-ZH5ebIfTjx02fMJexMG/To+AakmnedG0DH5Q/W3gUqRBimp0k5y/eUpvAmPXDdV9NnWy0IFxEZvKur0x6gidxA=="
|
||||
},
|
||||
"node_modules/@alpinejs/sort": {
|
||||
"version": "3.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.16.2.tgz",
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"vite": "^8.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ae_utbm/aemark": "^0.1.1",
|
||||
"@alpinejs/sort": "^3.16.2",
|
||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
|
||||
@@ -153,6 +153,7 @@ class UeFilterSchema(FilterSchema):
|
||||
set[Literal["CS", "TM", "EC", "OM", "QC"]] | None,
|
||||
FilterLookup("credit_type__in"),
|
||||
] = None
|
||||
is_open: bool | None = None
|
||||
language: str = "FR"
|
||||
department: Annotated[set[str] | None, FilterLookup("department__in")] = None
|
||||
|
||||
@@ -187,3 +188,10 @@ class UeFilterSchema(FilterSchema):
|
||||
return Q()
|
||||
value.add("AUTUMN_AND_SPRING")
|
||||
return Q(semester__in=value)
|
||||
|
||||
def filter_is_open(self, value: bool | None) -> Q: # noqa: FBT001
|
||||
if value is None:
|
||||
return Q()
|
||||
if not value:
|
||||
return Q(semester="CLOSED")
|
||||
return ~Q(semester="CLOSED")
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import {
|
||||
getCurrentUrlParams,
|
||||
History,
|
||||
updateQueryString,
|
||||
} from "#core:utils/history.ts";
|
||||
import { ueFetchUeList } from "#openapi";
|
||||
|
||||
const pageDefault = 1;
|
||||
const pageSizeDefault = 100;
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("ue_search", () => ({
|
||||
ues: {
|
||||
count: 0,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [],
|
||||
},
|
||||
loading: false,
|
||||
page: pageDefault,
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: pageSizeDefault,
|
||||
search: "",
|
||||
department: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
credit_type: [],
|
||||
semester: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
to_change: [],
|
||||
pushstate: History.Push,
|
||||
|
||||
update: undefined,
|
||||
|
||||
initializeArgs() {
|
||||
const url = getCurrentUrlParams();
|
||||
this.pushstate = History.Replace;
|
||||
|
||||
this.page = Number.parseInt(url.get("page"), 10) || pageDefault;
|
||||
this.page_size = Number.parseInt(url.get("page_size"), 10) || pageSizeDefault;
|
||||
this.search = url.get("search") || "";
|
||||
this.department = url.getAll("department");
|
||||
this.credit_type = url.getAll("credit_type");
|
||||
/* 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]).
|
||||
Thus there is some conversion involved when both communicate together */
|
||||
this.semester = url.has("semester") ? url.get("semester").split("_AND_") : [];
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.update = Alpine.debounce(async () => {
|
||||
/* Create the whole url before changing everything all at once */
|
||||
const first = this.to_change.shift();
|
||||
let url = updateQueryString(first.param, first.value, History.None);
|
||||
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 */
|
||||
this.to_change = [];
|
||||
this.pushstate = History.Push;
|
||||
}, 50);
|
||||
|
||||
const searchParams = ["search", "department", "credit_type", "semester"];
|
||||
const paginationParams = ["page", "page_size"];
|
||||
|
||||
for (const param of searchParams) {
|
||||
this.$watch(param, () => {
|
||||
if (this.pushstate !== History.Push) {
|
||||
/* This means that we are doing a mass param edit */
|
||||
return;
|
||||
}
|
||||
/* Reset pagination on search */
|
||||
this.page = pageDefault;
|
||||
this.page_size = pageSizeDefault;
|
||||
});
|
||||
}
|
||||
for (const param of searchParams.concat(paginationParams)) {
|
||||
this.$watch(param, (value) => {
|
||||
this.to_change.push({ param: param, value: value });
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
window.addEventListener("popstate", () => {
|
||||
this.initializeArgs();
|
||||
});
|
||||
this.initializeArgs();
|
||||
},
|
||||
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
const args = {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: this.page_size,
|
||||
};
|
||||
for (const [param, value] of new URL(
|
||||
window.location.href,
|
||||
).searchParams.entries()) {
|
||||
// Deal with array type params
|
||||
if (["credit_type", "department", "semester"].includes(param)) {
|
||||
if (args[param] === undefined) {
|
||||
args[param] = [];
|
||||
}
|
||||
args[param].push(value);
|
||||
} else {
|
||||
args[param] = value;
|
||||
}
|
||||
}
|
||||
this.ues = (await ueFetchUeList({ query: args })).data;
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
maxPage() {
|
||||
return Math.ceil(this.ues.count / this.page_size);
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { getCurrentUrlParams, updateQueryString } from "#core:utils/history";
|
||||
import { type SimpleUeSchema, ueFetchUeList } from "#openapi";
|
||||
|
||||
const pageDefault = 1;
|
||||
const pageSizeDefault = 100;
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("ue_search", () => ({
|
||||
ues: {
|
||||
count: 0,
|
||||
next: null as string | null,
|
||||
previous: null as string | null,
|
||||
results: [] as SimpleUeSchema[],
|
||||
},
|
||||
loading: false,
|
||||
page: pageDefault,
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: pageSizeDefault,
|
||||
search: "",
|
||||
hideClosedUes: true,
|
||||
department: [] as string[],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
credit_type: [] as string[],
|
||||
semester: [] as string[],
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
to_change: [] as { param: string; value: string }[],
|
||||
|
||||
// dummy implementation to make TS happy.
|
||||
// The real function is initialized in init
|
||||
update: () => {
|
||||
console.warn("Update not yet initialized");
|
||||
},
|
||||
|
||||
initializeArgs() {
|
||||
const url = getCurrentUrlParams();
|
||||
this.page = Number.parseInt(url.get("page") || pageDefault.toString(), 10);
|
||||
this.page_size = Number.parseInt(
|
||||
url.get("page_size") || pageSizeDefault.toString(),
|
||||
10,
|
||||
);
|
||||
this.search = url.get("search") || "";
|
||||
this.hideClosedUes = url.get("hideClosed") || true;
|
||||
this.department = url.getAll("department");
|
||||
this.credit_type = url.getAll("credit_type");
|
||||
/* 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]).
|
||||
Thus there is some conversion involved when both communicate together */
|
||||
this.semester = url.get("semester")?.split("_AND_") || [];
|
||||
|
||||
this.update();
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.update = Alpine.debounce(async () => {
|
||||
/* Create the whole url before changing everything all at once */
|
||||
for (const val of this.to_change) {
|
||||
updateQueryString(val.param, val.value);
|
||||
}
|
||||
await this.fetchData(); /* reload data on form change */
|
||||
this.to_change = [];
|
||||
}, 50);
|
||||
|
||||
const searchParams = [
|
||||
"search",
|
||||
"hideClosedUes",
|
||||
"department",
|
||||
"credit_type",
|
||||
"semester",
|
||||
];
|
||||
const paginationParams = ["page", "page_size"];
|
||||
|
||||
for (const param of searchParams) {
|
||||
this.$watch(param, () => {
|
||||
/* Reset pagination on search */
|
||||
this.page = pageDefault;
|
||||
this.page_size = pageSizeDefault;
|
||||
});
|
||||
}
|
||||
for (const param of searchParams.concat(paginationParams)) {
|
||||
this.$watch(param, (value: string) => {
|
||||
this.to_change.push({ param: param, value: value });
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
this.initializeArgs();
|
||||
},
|
||||
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
|
||||
const res = await ueFetchUeList({
|
||||
query: {
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
page_size: this.page_size,
|
||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||
credit_type: this.credit_type.length > 0 ? this.credit_type : undefined,
|
||||
semester: this.semester.length > 0 ? this.semester : undefined,
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake_case
|
||||
is_open: this.hideClosedUes ? true : undefined,
|
||||
department: this.department.length > 0 ? this.department : undefined,
|
||||
search: this.search || undefined,
|
||||
},
|
||||
});
|
||||
if (res.data !== undefined) {
|
||||
this.ues = res.data;
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
maxPage() {
|
||||
return Math.ceil(this.ues.count / this.page_size);
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -44,6 +44,10 @@
|
||||
x-model.debounce.500ms="search"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<input type="checkbox" class="switch" x-model="hideClosedUes" id="hide-closed-ues" name="hide-closed-ues">
|
||||
<label for="hide-closed-ues">{% trans %}Hide closed UEs{% endtrans %}</label>
|
||||
</fieldset>
|
||||
<div class="row gap-3x margin-bottom radio-guide">
|
||||
<fieldset>
|
||||
{% set departments = [
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@@ -64,14 +64,6 @@ class Subscription(models.Model):
|
||||
max_length=20,
|
||||
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:
|
||||
ordering = ["subscription_start"]
|
||||
|
||||
Reference in New Issue
Block a user