Skip to content

Commit 90befee

Browse files
committed
timetable base
1 parent 77bc184 commit 90befee

File tree

14 files changed

+222
-7
lines changed

14 files changed

+222
-7
lines changed

core/models.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,6 @@ def is_com_admin(self):
653653

654654

655655
class AnonymousUser(AuthAnonymousUser):
656-
def __init__(self):
657-
super().__init__()
658-
659656
@property
660657
def was_subscribed(self):
661658
return False
@@ -664,10 +661,6 @@ def was_subscribed(self):
664661
def is_subscribed(self):
665662
return False
666663

667-
@property
668-
def subscribed(self):
669-
return False
670-
671664
@property
672665
def is_root(self):
673666
return False

sith/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"pedagogy",
108108
"galaxy",
109109
"antispam",
110+
"timetable",
110111
)
111112

112113
MIDDLEWARE = (

sith/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
path("i18n/", include("django.conf.urls.i18n")),
6262
path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
6363
path("captcha/", include("captcha.urls")),
64+
path("timetable/", include(("timetable.urls", "timetable"), namespace="timetable")),
6465
]
6566

6667
if settings.DEBUG:

timetable/__init__.py

Whitespace-only changes.

timetable/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Register your models here.

timetable/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class TimetableConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "timetable"

timetable/migrations/__init__.py

Whitespace-only changes.

timetable/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your models here.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// see https://regex101.com/r/QHSaPM/2
2+
const TIMETABLE_ROW_RE: RegExp =
3+
/^(?<ueCode>[A-Z\d]{4}(?:\+[A-Z\d]{4})?)\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+(?:[\wé]*\s+)?(?<room>\w+(?:, \w+)?)$/;
4+
5+
const DEFAULT_TIMETABLE: string = `DS52\t\tCM1\t\tlundi\t08:00\t10:00\t1\tPrésentiel\tA113
6+
DS53\t\tCM1\t\tlundi\t10:15\t12:15\t1\tPrésentiel\tA101
7+
DS53\t\tTP1\t\tlundi\t13:00\t16:00\t1\tPrésentiel\tH010
8+
SO03\t\tCM1\t\tlundi\t16:15\t17:45\t1\tPrésentiel\tA103
9+
SO03\t\tTD1\t\tlundi\t17:45\t19:45\t1\tPrésentiel\tA103
10+
DS50\t\tTP1\t\tmardi\t08:00\t10:00\t1\tPrésentiel\tA216
11+
DS51\t\tCM1\t\tmardi\t10:15\t12:15\t1\tPrésentiel\tA216
12+
DS51\t\tTP1\t\tmardi\t14:00\t18:00\t1\tPrésentiel\tH010
13+
DS52\t\tTP2\tA\tjeudi\t08:00\t10:00\tA\tPrésentiel\tA110a, A110b
14+
DS52\t\tTD1\t\tjeudi\t10:15\t12:15\t1\tPrésentiel\tA110a, A110b
15+
LC02\t\tTP1\t\tjeudi\t15:00\t16:00\t1\tPrésentiel\tA209
16+
LC02\t\tTD1\t\tjeudi\t16:15\t18:15\t1\tPrésentiel\tA206`;
17+
18+
type WeekDay =
19+
| "lundi"
20+
| "mardi"
21+
| "mercredi"
22+
| "jeudi"
23+
| "vendredi"
24+
| "samedi"
25+
| "dimanche";
26+
27+
const WEEKDAYS = [
28+
"lundi",
29+
"mardi",
30+
"mercredi",
31+
"jeudi",
32+
"vendredi",
33+
"samedi",
34+
"dimanche",
35+
] as const;
36+
37+
const SLOT_HEIGHT = 20 as const; // Each 15min has a height of 20px in the timetable
38+
const SLOT_WIDTH = 400 as const; // Each weekday ha a width of 400px in the timetable
39+
const MINUTES_PER_SLOT = 15 as const;
40+
41+
interface TimetableSlot {
42+
courseType: string;
43+
room: string;
44+
startHour: string;
45+
endHour: string;
46+
startSlot: number;
47+
endSlot: number;
48+
ueCode: string;
49+
weekGroup?: string;
50+
weekday: WeekDay;
51+
}
52+
53+
function parseSlots(s: string): TimetableSlot[] {
54+
return s
55+
.split("\n")
56+
.filter((s: string) => s.length > 0)
57+
.map((row: string) => {
58+
const parsed = TIMETABLE_ROW_RE.exec(row);
59+
if (!parsed) {
60+
throw new Error(`Couldn't parse row ${row}`);
61+
}
62+
const [startHour, startMin] = parsed.groups.startHour
63+
.split(":")
64+
.map((i) => Number.parseInt(i));
65+
const [endHour, endMin] = parsed.groups.endHour
66+
.split(":")
67+
.map((i) => Number.parseInt(i));
68+
return {
69+
...parsed.groups,
70+
startSlot: Math.floor((startHour * 60 + startMin) / MINUTES_PER_SLOT),
71+
endSlot: Math.floor((endHour * 60 + endMin) / MINUTES_PER_SLOT),
72+
} as unknown as TimetableSlot;
73+
});
74+
}
75+
76+
document.addEventListener("alpine:init", () => {
77+
Alpine.data("timetableGenerator", () => ({
78+
content: DEFAULT_TIMETABLE,
79+
error: "",
80+
displayedWeekdays: [] as WeekDay[],
81+
courses: [] as TimetableSlot[],
82+
startSlot: 0,
83+
table: {
84+
height: 0,
85+
width: 0,
86+
},
87+
88+
generate() {
89+
try {
90+
this.courses = parseSlots(this.content);
91+
} catch {
92+
this.error = gettext(
93+
"Wrong timetable format. Make sure you copied if from your student folder.",
94+
);
95+
return;
96+
}
97+
this.displayedWeekdays = WEEKDAYS.filter((day) =>
98+
this.courses.some((slot: TimetableSlot) => slot.weekday === day),
99+
);
100+
this.startSlot = this.courses.reduce(
101+
(acc: number, curr: TimetableSlot) => Math.min(acc, curr.startSlot),
102+
24 * 4,
103+
);
104+
this.endSlot = this.courses.reduce(
105+
(acc: number, curr: TimetableSlot) => Math.max(acc, curr.endSlot),
106+
0,
107+
);
108+
this.table.height = SLOT_HEIGHT * (this.endSlot - this.startSlot);
109+
this.table.width = SLOT_WIDTH * this.displayedWeekdays.length;
110+
},
111+
112+
getStyle(slot: TimetableSlot) {
113+
return {
114+
height: `${(slot.endSlot - slot.startSlot) * SLOT_HEIGHT}px`,
115+
width: `${SLOT_WIDTH}px`,
116+
top: `${(slot.startSlot - this.startSlot) * SLOT_HEIGHT}px`,
117+
left: `${this.displayedWeekdays.indexOf(slot.weekday) * SLOT_WIDTH}px`,
118+
};
119+
},
120+
}));
121+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#timetable {
2+
.header {
3+
background-color: white;
4+
box-shadow: none;
5+
width: 100%;
6+
display: flex;
7+
flex-direction: row;
8+
gap: 0;
9+
span {
10+
flex: 1;
11+
text-align: center;
12+
}
13+
}
14+
.content {
15+
position: relative;
16+
text-align: center;
17+
18+
.slot {
19+
background-color: cadetblue;
20+
position: absolute;
21+
display: flex;
22+
flex-direction: column;
23+
justify-content: center;
24+
}
25+
}
26+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{% extends 'core/base.jinja' %}
2+
3+
{%- block additional_css -%}
4+
<link rel="stylesheet" href="{{ static('timetable/css/generator.scss') }}">
5+
{%- endblock -%}
6+
7+
{%- block additional_js -%}
8+
<script type="module" src="{{ static('bundled/timetable/generator-index.ts') }}"></script>
9+
{%- endblock -%}
10+
11+
{% block title %}
12+
{% trans %}Timeplan generator{% endtrans %}
13+
{% endblock %}
14+
15+
{% block content %}
16+
<div x-data="timetableGenerator">
17+
<form @submit.prevent="generate()">
18+
<h1>Générateur d'emploi du temps</h1>
19+
<div class="alert alert-red" x-show="!!error" x-cloak>
20+
<p class="alert-main" x-text="error"></p>
21+
</div>
22+
<div class="form-group">
23+
<label for="timetable-input">Colle ton emploi du temps (sans l'entête)</label>
24+
<textarea id="timetable-input" cols="30" rows="15" x-model="content"></textarea>
25+
</div>
26+
<input type="submit" class="btn btn-blue" value="{% trans %}Generate{% endtrans %}">
27+
</form>
28+
<div
29+
id="timetable"
30+
x-show="table.height > 0 && table.width > 0"
31+
:style="{width: `${table.width}px`, height: `${table.height}px`}"
32+
>
33+
<div class="header">
34+
<template x-for="weekday in displayedWeekdays">
35+
<span x-text="weekday"></span>
36+
</template>
37+
</div>
38+
<div class="content">
39+
<template x-for="course in courses">
40+
<div class="slot" :style="getStyle(course)">
41+
<span x-text="`${course.ueCode} (${course.courseType})`"></span>
42+
<span x-text="`${course.startHour} - ${course.endHour}`"></span>
43+
<span x-text="course.room"></span>
44+
</div>
45+
</template>
46+
</div>
47+
</div>
48+
</div>
49+
{% endblock content %}

timetable/tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your tests here.

timetable/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.urls import path
2+
3+
from timetable.views import GeneratorView
4+
5+
urlpatterns = [path("generator/", GeneratorView.as_view(), name="generator")]

timetable/views.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Create your views here.
2+
from django.contrib.auth.mixins import UserPassesTestMixin
3+
from django.views.generic import TemplateView
4+
5+
6+
class GeneratorView(UserPassesTestMixin, TemplateView):
7+
template_name = "timetable/generator.jinja"
8+
9+
def test_func(self):
10+
return self.request.user.is_subscribed

0 commit comments

Comments
 (0)