From 90befeeb8303cff0fc77f801ebc85ac4febe37b0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 3 Mar 2025 15:24:22 +0100 Subject: [PATCH] timetable base --- core/models.py | 7 - sith/settings.py | 1 + sith/urls.py | 1 + timetable/__init__.py | 0 timetable/admin.py | 1 + timetable/apps.py | 6 + timetable/migrations/__init__.py | 0 timetable/models.py | 1 + .../bundled/timetable/generator-index.ts | 121 ++++++++++++++++++ timetable/static/timetable/css/generator.scss | 26 ++++ timetable/templates/timetable/generator.jinja | 49 +++++++ timetable/tests.py | 1 + timetable/urls.py | 5 + timetable/views.py | 10 ++ 14 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 timetable/__init__.py create mode 100644 timetable/admin.py create mode 100644 timetable/apps.py create mode 100644 timetable/migrations/__init__.py create mode 100644 timetable/models.py create mode 100644 timetable/static/bundled/timetable/generator-index.ts create mode 100644 timetable/static/timetable/css/generator.scss create mode 100644 timetable/templates/timetable/generator.jinja create mode 100644 timetable/tests.py create mode 100644 timetable/urls.py create mode 100644 timetable/views.py diff --git a/core/models.py b/core/models.py index 4748f311..753b709e 100644 --- a/core/models.py +++ b/core/models.py @@ -653,9 +653,6 @@ class User(AbstractUser): class AnonymousUser(AuthAnonymousUser): - def __init__(self): - super().__init__() - @property def was_subscribed(self): return False @@ -664,10 +661,6 @@ class AnonymousUser(AuthAnonymousUser): def is_subscribed(self): return False - @property - def subscribed(self): - return False - @property def is_root(self): return False diff --git a/sith/settings.py b/sith/settings.py index 8191251f..a2c7a320 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -107,6 +107,7 @@ INSTALLED_APPS = ( "pedagogy", "galaxy", "antispam", + "timetable", ) MIDDLEWARE = ( diff --git a/sith/urls.py b/sith/urls.py index f6f7c8bb..3edaa883 100644 --- a/sith/urls.py +++ b/sith/urls.py @@ -61,6 +61,7 @@ urlpatterns = [ path("i18n/", include("django.conf.urls.i18n")), path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), path("captcha/", include("captcha.urls")), + path("timetable/", include(("timetable.urls", "timetable"), namespace="timetable")), ] if settings.DEBUG: diff --git a/timetable/__init__.py b/timetable/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/timetable/admin.py b/timetable/admin.py new file mode 100644 index 00000000..846f6b40 --- /dev/null +++ b/timetable/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/timetable/apps.py b/timetable/apps.py new file mode 100644 index 00000000..b39d6c1e --- /dev/null +++ b/timetable/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TimetableConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "timetable" diff --git a/timetable/migrations/__init__.py b/timetable/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/timetable/models.py b/timetable/models.py new file mode 100644 index 00000000..6b202199 --- /dev/null +++ b/timetable/models.py @@ -0,0 +1 @@ +# Create your models here. diff --git a/timetable/static/bundled/timetable/generator-index.ts b/timetable/static/bundled/timetable/generator-index.ts new file mode 100644 index 00000000..19d716c0 --- /dev/null +++ b/timetable/static/bundled/timetable/generator-index.ts @@ -0,0 +1,121 @@ +// see https://regex101.com/r/QHSaPM/2 +const TIMETABLE_ROW_RE: RegExp = + /^(?[A-Z\d]{4}(?:\+[A-Z\d]{4})?)\s+(?[A-Z]{2}\d)\s+((?[AB])\s+)?(?(lundi)|(mardi)|(mercredi)|(jeudi)|(vendredi)|(samedi)|(dimanche))\s+(?\d{2}:\d{2})\s+(?\d{2}:\d{2})\s+[\dA-B]\s+(?:[\wé]*\s+)?(?\w+(?:, \w+)?)$/; + +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\tTP1\t\tlundi\t13:00\t16:00\t1\tPrésentiel\tH010 +SO03\t\tCM1\t\tlundi\t16:15\t17:45\t1\tPrésentiel\tA103 +SO03\t\tTD1\t\tlundi\t17:45\t19:45\t1\tPrésentiel\tA103 +DS50\t\tTP1\t\tmardi\t08:00\t10:00\t1\tPrésentiel\tA216 +DS51\t\tCM1\t\tmardi\t10:15\t12:15\t1\tPrésentiel\tA216 +DS51\t\tTP1\t\tmardi\t14:00\t18:00\t1\tPrésentiel\tH010 +DS52\t\tTP2\tA\tjeudi\t08:00\t10:00\tA\tPrésentiel\tA110a, A110b +DS52\t\tTD1\t\tjeudi\t10:15\t12:15\t1\tPrésentiel\tA110a, A110b +LC02\t\tTP1\t\tjeudi\t15:00\t16:00\t1\tPrésentiel\tA209 +LC02\t\tTD1\t\tjeudi\t16:15\t18:15\t1\tPrésentiel\tA206`; + +type WeekDay = + | "lundi" + | "mardi" + | "mercredi" + | "jeudi" + | "vendredi" + | "samedi" + | "dimanche"; + +const WEEKDAYS = [ + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi", + "dimanche", +] as const; + +const SLOT_HEIGHT = 20 as const; // Each 15min has a height of 20px in the timetable +const SLOT_WIDTH = 400 as const; // Each weekday ha a width of 400px in the timetable +const MINUTES_PER_SLOT = 15 as const; + +interface TimetableSlot { + courseType: string; + room: string; + startHour: string; + endHour: string; + startSlot: number; + endSlot: number; + ueCode: string; + weekGroup?: string; + weekday: WeekDay; +} + +function parseSlots(s: string): TimetableSlot[] { + return s + .split("\n") + .filter((s: string) => s.length > 0) + .map((row: string) => { + const parsed = TIMETABLE_ROW_RE.exec(row); + if (!parsed) { + throw new Error(`Couldn't parse row ${row}`); + } + const [startHour, startMin] = parsed.groups.startHour + .split(":") + .map((i) => Number.parseInt(i)); + const [endHour, endMin] = parsed.groups.endHour + .split(":") + .map((i) => Number.parseInt(i)); + return { + ...parsed.groups, + startSlot: Math.floor((startHour * 60 + startMin) / MINUTES_PER_SLOT), + endSlot: Math.floor((endHour * 60 + endMin) / MINUTES_PER_SLOT), + } as unknown as TimetableSlot; + }); +} + +document.addEventListener("alpine:init", () => { + Alpine.data("timetableGenerator", () => ({ + content: DEFAULT_TIMETABLE, + error: "", + displayedWeekdays: [] as WeekDay[], + courses: [] as TimetableSlot[], + startSlot: 0, + table: { + height: 0, + width: 0, + }, + + generate() { + try { + this.courses = parseSlots(this.content); + } catch { + this.error = gettext( + "Wrong timetable format. Make sure you copied if from your student folder.", + ); + return; + } + this.displayedWeekdays = WEEKDAYS.filter((day) => + this.courses.some((slot: TimetableSlot) => slot.weekday === day), + ); + this.startSlot = this.courses.reduce( + (acc: number, curr: TimetableSlot) => Math.min(acc, curr.startSlot), + 24 * 4, + ); + this.endSlot = this.courses.reduce( + (acc: number, curr: TimetableSlot) => Math.max(acc, curr.endSlot), + 0, + ); + this.table.height = SLOT_HEIGHT * (this.endSlot - this.startSlot); + this.table.width = SLOT_WIDTH * this.displayedWeekdays.length; + }, + + getStyle(slot: TimetableSlot) { + return { + height: `${(slot.endSlot - slot.startSlot) * SLOT_HEIGHT}px`, + width: `${SLOT_WIDTH}px`, + top: `${(slot.startSlot - this.startSlot) * SLOT_HEIGHT}px`, + left: `${this.displayedWeekdays.indexOf(slot.weekday) * SLOT_WIDTH}px`, + }; + }, + })); +}); diff --git a/timetable/static/timetable/css/generator.scss b/timetable/static/timetable/css/generator.scss new file mode 100644 index 00000000..ae5fb83f --- /dev/null +++ b/timetable/static/timetable/css/generator.scss @@ -0,0 +1,26 @@ +#timetable { + .header { + background-color: white; + box-shadow: none; + width: 100%; + display: flex; + flex-direction: row; + gap: 0; + span { + flex: 1; + text-align: center; + } + } + .content { + position: relative; + text-align: center; + + .slot { + background-color: cadetblue; + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + } + } +} diff --git a/timetable/templates/timetable/generator.jinja b/timetable/templates/timetable/generator.jinja new file mode 100644 index 00000000..81f72707 --- /dev/null +++ b/timetable/templates/timetable/generator.jinja @@ -0,0 +1,49 @@ +{% extends 'core/base.jinja' %} + +{%- block additional_css -%} + +{%- endblock -%} + +{%- block additional_js -%} + +{%- endblock -%} + +{% block title %} + {% trans %}Timeplan generator{% endtrans %} +{% endblock %} + +{% block content %} +
+
+

Générateur d'emploi du temps

+
+

+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+
+{% endblock content %} diff --git a/timetable/tests.py b/timetable/tests.py new file mode 100644 index 00000000..a39b155a --- /dev/null +++ b/timetable/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/timetable/urls.py b/timetable/urls.py new file mode 100644 index 00000000..5e141005 --- /dev/null +++ b/timetable/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from timetable.views import GeneratorView + +urlpatterns = [path("generator/", GeneratorView.as_view(), name="generator")] diff --git a/timetable/views.py b/timetable/views.py new file mode 100644 index 00000000..351f199a --- /dev/null +++ b/timetable/views.py @@ -0,0 +1,10 @@ +# Create your views here. +from django.contrib.auth.mixins import UserPassesTestMixin +from django.views.generic import TemplateView + + +class GeneratorView(UserPassesTestMixin, TemplateView): + template_name = "timetable/generator.jinja" + + def test_func(self): + return self.request.user.is_subscribed