2 Commits

Author SHA1 Message Date
b52dbdd4fc allow export to Png 2025-06-27 11:47:52 +02:00
db6c356f73 timetable base 2025-06-26 18:43:23 +02:00
18 changed files with 302 additions and 116 deletions

View File

@ -636,9 +636,6 @@ class User(AbstractUser):
class AnonymousUser(AuthAnonymousUser):
def __init__(self):
super().__init__()
@property
def was_subscribed(self):
return False
@ -647,10 +644,6 @@ class AnonymousUser(AuthAnonymousUser):
def is_subscribed(self):
return False
@property
def subscribed(self):
return False
@property
def is_root(self):
return False

View File

@ -1,108 +0,0 @@
Cette page expose la politique du Pôle informatique de l'AE
en ce qui concerne l'usage et l'implémentation de systèmes d'IA
dans le cadre de l'AE et du développement de ses outils.
## Cadre
En accord avec le règlement européen sur
l'intelligence artificielle du 13 juin 2024,
nous définissons comme IA :
> Un système basé sur une machine qui est
> conçu pour fonctionner avec différents niveaux d'autonomie
> et qui peut faire preuve d'adaptabilité après son déploiement,
> et qui, pour des objectifs explicites ou implicites, déduit,
> à partir des données qu'il reçoit,
> comment générer des résultats tels que des prédictions,
> du contenu, des recommandations ou des décisions
> qui peuvent influencer des environnements physiques ou virtuels.
Cette définition recouvre toutes les IAs génératives, ce qui inclut
ChatGPT, DeepSeek, Claude, Copilot, Llama et autres outils similaires.
## Utilisation dans le développement
!!!danger
La soumission de code généré par IA est strictement interdite.
Aucune contribution contenant du code généré par IA n'est acceptée.
Toute PR contenant en proportion significative du code duquel
on peut raisonnablement penser qu'il a été généré par IA
pourra être refusée sans aucun autre motif.
Bien que nous ne puissions pas l'interdire,
nous déconseillons également fortement l'usage de tout
recours à un système d'IA dans le processus de développement,
quel que soit son usage (debug, recherche d'information ou autres).
Référez-vous en priorité à la documentation du site,
à celle de Django et à l'aide des autres développeurs,
mais par pitié, ne faites jamais appel à l'IA.
## Intégration dans le site
L'intégration sur le site AE de systèmes d'IA
et de toute fonctionnalité basée sur des systèmes d'IA
est strictement prohibée, quel qu'en soit l'objectif.
Toute tâche de modération, de génération
ou de détection de contenu ne doit être accomplie
par des êtres humains ou par des algorithmes
déterministes, testés et compris.
L'usage des données du site a des fins d'entrainement d'IA,
ainsi que la transmission de ces données à un système d'IA
est strictement interdit.
Tout acte de cette nature sera considéré comme une violation
grave de la politique de gestion des données de l'AE.
## Motifs de cette politique
Le site AE est un programme écrit par des humains, pour des humains.
C'est un logiciel dont la complexité nécessite des connaissances
plus approfondies que ce qui est attendu de la part d'un
étudiant en TC ou en base branche.
À ce titre, l'interdiction de l'IA dans le cadre de son
développement est pensée avant tout dans une optique
de formation des développeurs, de stabilité de la base de code
et de transmission des connaissances.
Nous ferons ici abstraction de l'impact écologique néfaste de l'IA,
qui n'en reste pas moins préoccupant et qui renforce
les autres motifs ayant poussé à interdire l'IA dans le cadre de l'AE.
### Formation des développeurs
Travailler sur le site AE est possiblement le meilleur moyen de
monter en compétences en informatique pour un étudiant de l'UTBM.
Automatisation des tests, gestion des données et de la sécurité,
infrastructure, maintenance du code existant...
Le site AE est un logiciel complet, dont le développement
possède une dimension pédagogique réelle.
En utilisant l'IA, le développement n'est plus un moyen efficace
de se former.
### Stabilité de la base de code
Les développeurs du site AE sont pour la plupart en cours de formation,
sans compréhension globale de la base de code du site,
des outils logiciels sur lesquels il se base et des bonnes
pratiques permettant d'écrire du code viable.
En se reposant sur un système d'IA sans être capacité
de comprendre intégralement le code proposé ni de le mettre
en perspective avec le reste de la base de code,
c'est toute la maintenance de la base de code qui se retrouve compromise.
### Transmission des connaissances
L'équipe du pôle informatique se renouvelle très souvent.
À ce titre, les nouveaux développeurs se doivent d'hériter
d'une base de code viable.
Quant aux anciens développeurs, ils se doivent d'en avoir
compris le fonctionnement, afin d'être en mesure
de guider et d'aider leurs successeurs.
Comme développé dans les deux points précédents,
cet objectif est incompatible avec l'usage de systèmes d'IA.

View File

@ -57,7 +57,6 @@ nav:
- Accueil: explanation/index.md
- Technologies utilisées: explanation/technos.md
- Conventions: explanation/conventions.md
- Politique IA: explanation/ia.md
- Archives: explanation/archives.md
- Tutoriels:
- Installer le projet: tutorial/install.md

57
package-lock.json generated
View File

@ -29,6 +29,8 @@
"d3-force-3d": "^3.0.5",
"easymde": "^2.19.0",
"glob": "^11.0.0",
"html-to-image": "^1.11.13",
"html2canvas": "^1.4.1",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
@ -3083,6 +3085,15 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -3471,6 +3482,15 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cytoscape": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz",
@ -4149,6 +4169,25 @@
"node": ">= 0.4"
}
},
"node_modules/html-to-image": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
"license": "MIT"
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmx.org": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
@ -5451,6 +5490,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/three": {
"version": "0.177.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.177.0.tgz",
@ -5708,6 +5756,15 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -59,6 +59,7 @@
"d3-force-3d": "^3.0.5",
"easymde": "^2.19.0",
"glob": "^11.0.0",
"html2canvas": "^1.4.1",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",

View File

@ -124,6 +124,7 @@ INSTALLED_APPS = (
"pedagogy",
"galaxy",
"antispam",
"timetable",
"api",
)

View File

@ -49,6 +49,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:

0
timetable/__init__.py Normal file
View File

1
timetable/admin.py Normal file
View File

@ -0,0 +1 @@
# Register your models here.

6
timetable/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TimetableConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "timetable"

View File

1
timetable/models.py Normal file
View File

@ -0,0 +1 @@
# Create your models here.

View File

@ -0,0 +1,134 @@
import html2canvas from "html2canvas";
// see https://regex101.com/r/QHSaPM/2
const TIMETABLE_ROW_RE: RegExp =
/^(?<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+)?)$/;
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`,
};
},
async savePng() {
const elem = document.getElementById("timetable");
const img = (await html2canvas(elem)).toDataURL();
const downloadLink = document.createElement("a");
downloadLink.href = img;
downloadLink.download = "edt.png";
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
},
}));
});

View File

@ -0,0 +1,28 @@
#timetable {
display: block;
margin: 2em auto ;
.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;
}
}
}

View File

@ -0,0 +1,56 @@
{% extends 'core/base.jinja' %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ static('timetable/css/generator.scss') }}">
{%- endblock -%}
{%- block additional_js -%}
<script type="module" src="{{ static('bundled/timetable/generator-index.ts') }}"></script>
{%- endblock -%}
{% block title %}
{% trans %}Timeplan generator{% endtrans %}
{% endblock %}
{% block content %}
<div x-data="timetableGenerator">
<form @submit.prevent="generate()">
<h1>Générateur d'emploi du temps</h1>
<div class="alert alert-red" x-show="!!error" x-cloak>
<p class="alert-main" x-text="error"></p>
</div>
<div class="form-group">
<label for="timetable-input">Colle ton emploi du temps (sans l'entête)</label>
<textarea id="timetable-input" cols="30" rows="15" x-model="content"></textarea>
</div>
<input type="submit" class="btn btn-blue" value="{% trans %}Generate{% endtrans %}">
</form>
<div
id="timetable"
x-show="table.height > 0 && table.width > 0"
:style="{width: `${table.width}px`, height: `${table.height+40}px`}"
>
<div class="header">
<template x-for="weekday in displayedWeekdays">
<span x-text="weekday"></span>
</template>
</div>
<div class="content">
<template x-for="course in courses">
<div class="slot" :style="getStyle(course)">
<span x-text="`${course.ueCode} (${course.courseType})`"></span>
<span x-text="`${course.startHour} - ${course.endHour}`"></span>
<span x-text="course.room"></span>
</div>
</template>
</div>
</div>
<button
class="margin-bottom btn btn-blue"
@click="savePng"
x-show="table.height > 0 && table.width > 0"
>
{% trans %}Save to PNG{% endtrans %}
</button>
</div>
{% endblock content %}

1
timetable/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

5
timetable/urls.py Normal file
View File

@ -0,0 +1,5 @@
from django.urls import path
from timetable.views import GeneratorView
urlpatterns = [path("generator/", GeneratorView.as_view(), name="generator")]

10
timetable/views.py Normal file
View File

@ -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