mirror of
https://github.com/ae-utbm/sith.git
synced 2026-10-10 12:50:29 +00:00
api to save timetable
This commit is contained in:
1 parent
05d6c01957
commit
f29010b5dc
3 files changed
+147
-3
No files matched your search
@@ -0,0 +1,37 @@
|
|||||||
|
from ninja.security import SessionAuth
|
||||||
|
from ninja_extra import ControllerBase, api_controller, route
|
||||||
|
from ninja_extra.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from core.utils import get_semester_code
|
||||||
|
from timetable.models import Timetable, TimetableSlot
|
||||||
|
from timetable.schemas import CreateTimetableSchema, TimetableSchema
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/edt", urls_namespace="timetable")
|
||||||
|
class TimetableController(ControllerBase):
|
||||||
|
@route.put(
|
||||||
|
"/{int:user_id}",
|
||||||
|
auth=SessionAuth(),
|
||||||
|
response={200: TimetableSchema},
|
||||||
|
url_name="save",
|
||||||
|
)
|
||||||
|
def save_timetable(self, user_id: int, data: CreateTimetableSchema):
|
||||||
|
user = self.context.request.user
|
||||||
|
if not user.has_perm("pedagogy.add_timetable") and not (
|
||||||
|
user.has_perm("timetable.add_self_timetable") and user.id == user_id
|
||||||
|
):
|
||||||
|
raise PermissionDenied
|
||||||
|
timetable, created = Timetable.objects.get_or_create(
|
||||||
|
semester=get_semester_code(), user_id=user_id
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
timetable.slots.all().delete()
|
||||||
|
if user.preferences.show_my_timetable != data.is_viewable:
|
||||||
|
user.preferences.show_my_timetable = data.is_viewable
|
||||||
|
user.preferences.save()
|
||||||
|
slots = TimetableSlot.objects.bulk_create(
|
||||||
|
[TimetableSlot(timetable=timetable, **s.model_dump()) for s in data.slots]
|
||||||
|
)
|
||||||
|
return TimetableSchema(
|
||||||
|
id=timetable.id, semester=timetable.semester, slots=slots
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from annotated_types import Ge, Le
|
||||||
|
from ninja import ModelSchema, Schema
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from timetable.models import Timetable, TimetableSlot
|
||||||
|
|
||||||
|
|
||||||
|
class TimetableSlotSchema(ModelSchema):
|
||||||
|
weekday: TimetableSlot.WeekDay = Field(
|
||||||
|
description=(
|
||||||
|
f"{TimetableSlot.WeekDay.MONDAY} is monday, "
|
||||||
|
f"{TimetableSlot.WeekDay.TUESDAY} is tuesday, etc."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
start_at: Annotated[int, Ge(0), Le(96)]
|
||||||
|
end_at: Annotated[int, Ge(0), Le(96)]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TimetableSlot
|
||||||
|
fields = ["start_at", "end_at", "week_group", "ue", "course_type", "room"]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTimetableSchema(Schema):
|
||||||
|
is_viewable: bool
|
||||||
|
slots: list[TimetableSlotSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class TimetableSchema(ModelSchema):
|
||||||
|
slots: list[TimetableSlotSchema]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Timetable
|
||||||
|
fields = ["id", "semester"]
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import html2canvas from "html2canvas";
|
import html2canvas from "html2canvas";
|
||||||
|
import { AlertMessage } from "#core:utils/alert-message";
|
||||||
|
import { type WeekDay as ApiWeekDay, timetableSaveTimetable } from "#openapi";
|
||||||
|
|
||||||
// see https://regex101.com/r/QHSaPM/3
|
// see https://regex101.com/r/QHSaPM/3
|
||||||
const TIMETABLE_ROW_RE: RegExp =
|
const TIMETABLE_ROW_RE: RegExp =
|
||||||
@@ -78,7 +80,76 @@ function parseSlots(s: string): TimetableSlot[] {
|
|||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("timetableGenerator", () => ({
|
Alpine.data("timetableGenerator", () => ({
|
||||||
content: DEFAULT_TIMETABLE,
|
content: DEFAULT_TIMETABLE,
|
||||||
error: null as { incorrectRow?: string },
|
error: null as { incorrectRow?: string } | null,
|
||||||
|
courses: [] as TimetableSlot[],
|
||||||
|
edtPublic: false,
|
||||||
|
saving: false,
|
||||||
|
timetableSavedMessage: new AlertMessage(),
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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) => ({
|
||||||
|
// 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) as ApiWeekDay,
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||||
|
week_group: c.weekGroup,
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||||
|
course_type: c.courseType,
|
||||||
|
room: c.room,
|
||||||
|
ue: c.ueCode,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpine data dedicated to timetable display only.
|
||||||
|
* It can interact with the `timetableGenerator` data with x-model and x-modelable
|
||||||
|
* (for example of this, see timetable/generator.jinja)
|
||||||
|
*/
|
||||||
|
Alpine.data("timetableDisplay", (slots: TimetableSlot[]) => ({
|
||||||
displayedWeekdays: [] as WeekDay[],
|
displayedWeekdays: [] as WeekDay[],
|
||||||
courses: [] as TimetableSlot[],
|
courses: [] as TimetableSlot[],
|
||||||
startSlot: 0,
|
startSlot: 0,
|
||||||
@@ -108,7 +179,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.courses = parseSlots(this.content);
|
this.courses = parseSlots(this.content);
|
||||||
this.error = null;
|
this.error = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = { incorrectRow: err?.cause?.row };
|
type ParseRowError = { cause: { row: string } };
|
||||||
|
this.error = { incorrectRow: (err as ParseRowError)?.cause?.row };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +210,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getStyle(slot: TimetableSlot) {
|
getStyle(slot: TimetableSlot) {
|
||||||
const hasWeekGroup = slot.weekGroup !== undefined;
|
const hasWeekGroup = !!slot.weekGroup;
|
||||||
const width = hasWeekGroup ? SLOT_WIDTH / 2 : SLOT_WIDTH;
|
const width = hasWeekGroup ? SLOT_WIDTH / 2 : SLOT_WIDTH;
|
||||||
const leftOffset = slot.weekGroup === "B" ? SLOT_WIDTH / 2 : 0;
|
const leftOffset = slot.weekGroup === "B" ? SLOT_WIDTH / 2 : 0;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user