# -*- 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//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"""
{group.get_nom_with_part(default="promo")}
""" if group else f"""
{edt_group} {scu.EMO_WARNING} non reconnu
""" ) # --- ModuleImpl edt_module = extract_event_module(event) modimpl: ModuleImpl = edt2modimpl.get(edt_module, None) mod_disp = ( f"""
{modimpl.module.code}
""" if modimpl else f"""
{scu.EMO_WARNING} {edt_module}
""" ) 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