2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
2023-01-02 13:16:27 +01:00
|
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
#
|
|
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""Accès aux emplois du temps
|
|
|
|
|
|
|
|
XXX usage uniquement experimental pour tests implémentations
|
|
|
|
|
|
|
|
"""
|
2023-11-11 18:13:18 +01:00
|
|
|
import re
|
2020-09-26 16:19:37 +02:00
|
|
|
import icalendar
|
2023-11-06 22:05:38 +01:00
|
|
|
|
2023-11-11 18:13:18 +01:00
|
|
|
from flask import flash
|
2021-08-29 19:57:32 +02:00
|
|
|
from app import log
|
2023-11-11 18:13:18 +01:00
|
|
|
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
|
|
|
|
import app.scodoc.sco_utils as scu
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2023-11-11 18:13:18 +01:00
|
|
|
def formsemestre_load_calendar(
|
|
|
|
formsemestre: FormSemestre,
|
|
|
|
) -> icalendar.cal.Calendar | None:
|
|
|
|
"""Load ics data, return calendar or None if not configured or not available"""
|
|
|
|
edt_id = formsemestre.get_edt_id()
|
|
|
|
if not edt_id:
|
|
|
|
flash(
|
|
|
|
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
return None
|
2023-11-11 18:13:18 +01:00
|
|
|
edt_ics_path = ScoDocSiteConfig.get("edt_ics_path")
|
|
|
|
if not edt_ics_path.strip():
|
|
|
|
return None
|
|
|
|
ics_filename = edt_ics_path.format(edt_id=edt_id)
|
2020-09-26 16:19:37 +02:00
|
|
|
try:
|
2023-11-11 18:13:18 +01:00
|
|
|
with open(ics_filename, "rb") as file:
|
|
|
|
log(f"Loading edt from {ics_filename}")
|
|
|
|
calendar = icalendar.Calendar.from_ical(file.read())
|
|
|
|
except FileNotFoundError:
|
|
|
|
flash("erreur chargement du calendrier")
|
2020-09-26 16:19:37 +02:00
|
|
|
log(
|
2023-11-11 18:13:18 +01:00
|
|
|
f"formsemestre_load_calendar: ics not found for {formsemestre}\npath='{ics_filename}'"
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
return None
|
2023-11-11 18:13:18 +01:00
|
|
|
|
|
|
|
return calendar
|
|
|
|
|
|
|
|
|
|
|
|
_COLOR_PALETTE = [
|
|
|
|
"#ff6961",
|
|
|
|
"#ffb480",
|
|
|
|
"#f8f38d",
|
|
|
|
"#42d6a4",
|
|
|
|
"#08cad1",
|
|
|
|
"#59adf6",
|
|
|
|
"#9d94ff",
|
|
|
|
"#c780e8",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
|
|
|
|
"""EDT complet du semestre, comme une liste de dict serialisable en json.
|
|
|
|
Fonction appellée par l'API /formsemestre/<int:formsemestre_id>/edt
|
|
|
|
TODO: spécifier intervalle de dates start et end
|
|
|
|
TODO: cacher ?
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2023-11-11 18:13:18 +01:00
|
|
|
# Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
|
|
|
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
|
|
|
group_colors = {
|
|
|
|
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
|
|
|
|
for i, group_name in enumerate(edt2group)
|
|
|
|
}
|
|
|
|
default_group = formsemestre.get_default_group()
|
|
|
|
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
|
|
|
|
|
|
|
# Chargement du calendier ics
|
|
|
|
calendar = formsemestre_load_calendar(formsemestre)
|
|
|
|
if not calendar:
|
|
|
|
return []
|
|
|
|
# Génération des événements, avec titre et champs utiles pour l'affichage dans ScoDoc
|
|
|
|
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
|
|
|
events_dict = []
|
|
|
|
for event in events:
|
|
|
|
if "DESCRIPTION" in event:
|
|
|
|
# --- Group
|
|
|
|
edt_group = extract_event_group(event)
|
|
|
|
# si pas de groupe dans l'event, prend toute la promo ("tous")
|
|
|
|
group: GroupDescr = (
|
|
|
|
edt2group.get(edt_group, None) if edt_group else default_group
|
|
|
|
)
|
|
|
|
background_color = (
|
|
|
|
group_colors.get(edt_group, "rgb(214, 233, 248)")
|
|
|
|
if group
|
|
|
|
else "lightgrey"
|
|
|
|
)
|
|
|
|
group_disp = (
|
|
|
|
f"""<div class="group-name">{group.get_nom_with_part(default="promo")}</div>"""
|
|
|
|
if group
|
|
|
|
else f"""<div class="group-edt">{edt_group}
|
|
|
|
<span title="vérifier noms de groupe ou configuration extraction edt">
|
|
|
|
{scu.EMO_WARNING} non reconnu</span>
|
|
|
|
</div>"""
|
|
|
|
)
|
|
|
|
# --- ModuleImpl
|
|
|
|
edt_module = extract_event_module(event)
|
|
|
|
modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
|
|
|
|
mod_disp = (
|
|
|
|
f"""<div class="module-edt mod-name" title="{modimpl.module.abbrev or ""}">{modimpl.module.code}
|
|
|
|
</div>"""
|
|
|
|
if modimpl
|
|
|
|
else f"""<div class="module-edt mod-etd" title="vérifier code edt module ?">{scu.EMO_WARNING} {edt_module}</div>"""
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
d = {
|
2023-11-11 18:13:18 +01:00
|
|
|
# Champs utilisés par tui.calendar
|
|
|
|
"calendarId": "cal1",
|
|
|
|
"title": extract_event_title(event) + group_disp + mod_disp,
|
|
|
|
"start": event.decoded("dtstart").isoformat(),
|
|
|
|
"end": event.decoded("dtend").isoformat(),
|
|
|
|
"backgroundColor": background_color,
|
|
|
|
# Infos brutes pour usage API éventuel
|
|
|
|
"group_id": group.id if group else None,
|
|
|
|
"group_edt_id": edt_group,
|
|
|
|
"moduleimpl_id": modimpl.id if modimpl else None,
|
2020-09-26 16:19:37 +02:00
|
|
|
}
|
2023-11-11 18:13:18 +01:00
|
|
|
events_dict.append(d)
|
|
|
|
|
|
|
|
return events_dict
|
|
|
|
|
|
|
|
|
|
|
|
def extract_event_title(event: icalendar.cal.Event) -> str:
|
|
|
|
"""Extrait le titre à afficher dans nos calendriers (si on ne retrouve pas le module ScoDoc)
|
|
|
|
En effet, le titre présent dans l'ics emploi du temps est souvent complexe et peu parlant.
|
|
|
|
Par exemple, à l'USPN, Hyperplanning nous donne:
|
|
|
|
'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
|
|
|
"""
|
|
|
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
|
|
|
if not event.has_key("DESCRIPTION"):
|
|
|
|
return "-"
|
|
|
|
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
|
|
|
# ici on prend le nom du module
|
|
|
|
m = re.search(r"Matière : \w+ - ([\w\.\s']+)", description)
|
|
|
|
if m and len(m.groups()) > 0:
|
|
|
|
return m.group(1)
|
|
|
|
# fallback: full description
|
|
|
|
return description
|
|
|
|
|
|
|
|
|
|
|
|
def extract_event_module(event: icalendar.cal.Event) -> str:
|
|
|
|
"""Extrait le code module de l'emplois du temps.
|
|
|
|
Chaine vide si ne le trouve pas.
|
|
|
|
Par exemple, à l'USPN, Hyperplanning nous donne le code 'VRETR113' dans DESCRIPTION
|
|
|
|
'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
|
|
|
"""
|
|
|
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
|
|
|
if not event.has_key("DESCRIPTION"):
|
|
|
|
return "-"
|
|
|
|
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
|
|
|
# extraction du code:
|
|
|
|
m = re.search(r"Matière : ([A-Z][A-Z0-9]+)", description)
|
|
|
|
if m and len(m.groups()) > 0:
|
|
|
|
return m.group(1)
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
def extract_event_group(event: icalendar.cal.Event) -> str:
|
|
|
|
"""Extrait le nom du groupe (TD, ...). "" si pas de match."""
|
|
|
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
|
|
|
# Utilise ici le SUMMARY
|
|
|
|
# qui est de la forme
|
|
|
|
# SUMMARY;LANGUAGE=fr:TP2 GPR1 - VCYR303 - Services reseaux ava (VCYR303) - 1234 - M. VIENNET EMMANUEL - V2ROM - BUT2 RT pa. ROM - Groupe 1
|
|
|
|
if not event.has_key("SUMMARY"):
|
|
|
|
return "-"
|
|
|
|
summary = event.decoded("SUMMARY").decode("utf-8") # assume ics in utf8
|
|
|
|
# extraction du code:
|
|
|
|
m = re.search(r".*- ([\w\s]+)$", summary)
|
|
|
|
if m and len(m.groups()) > 0:
|
|
|
|
return m.group(1).strip()
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
def formsemestre_retreive_modimpls_from_edt_id(
|
|
|
|
formsemestre: FormSemestre,
|
|
|
|
) -> dict[str, ModuleImpl]:
|
|
|
|
"""Construit un dict donnant le moduleimpl de chaque edt_id"""
|
|
|
|
edt2modimpl = {modimpl.get_edt_id(): modimpl for modimpl in formsemestre.modimpls}
|
|
|
|
edt2modimpl.pop("", None)
|
|
|
|
return edt2modimpl
|
|
|
|
|
|
|
|
|
|
|
|
def formsemestre_retreive_groups_from_edt_id(
|
|
|
|
formsemestre: FormSemestre,
|
|
|
|
) -> dict[str, GroupDescr]:
|
|
|
|
"""Construit un dict donnant le groupe de chaque edt_id"""
|
|
|
|
edt2group = {}
|
|
|
|
for partition in formsemestre.partitions:
|
|
|
|
edt2group.update({g.get_edt_id(): g for g in partition.groups})
|
|
|
|
edt2group.pop("", None)
|
|
|
|
return edt2group
|