ScoDoc-PE/app/scodoc/sco_edt_cal.py

220 lines
8.4 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
#
# 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
"""
import re
import icalendar
from flask import flash
from app import log
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
import app.scodoc.sco_utils as scu
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)"
)
return None
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)
try:
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")
log(
f"formsemestre_load_calendar: ics not found for {formsemestre}\npath='{ics_filename}'"
)
return None
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 ?
"""
# 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>"""
)
d = {
# 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,
}
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