EDT: construction des calendriers enseignants
This commit is contained in:
parent
9987a26d9e
commit
85f0323a80
@ -141,6 +141,16 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
||||
validators=[Optional(), check_ics_path],
|
||||
)
|
||||
edt_ics_user_path = StringField(
|
||||
label="Chemin vers les ics des utilisateurs (enseignants)",
|
||||
description="""Optionnel. Chemin absolu unix sur le serveur vers le fichier ics donnant l'emploi
|
||||
du temps d'un enseignant. La balise <tt>{edt_id}</tt> sera remplacée par l'edt_id du
|
||||
de l'utilisateur.
|
||||
Dans certains cas (XXX), ScoDoc peut générer ces fichiers et les écrira suivant
|
||||
ce chemin (avec edt_id).
|
||||
""",
|
||||
validators=[Optional(), check_ics_path],
|
||||
)
|
||||
|
||||
edt_ics_title_field = StringField(
|
||||
label="Champ contenant le titre",
|
||||
|
@ -31,6 +31,8 @@ Lecture et conversion des ics.
|
||||
|
||||
"""
|
||||
from datetime import timezone
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import icalendar
|
||||
@ -51,6 +53,33 @@ def get_ics_filename(edt_id: str) -> str | None:
|
||||
return edt_ics_path.format(edt_id=edt_id)
|
||||
|
||||
|
||||
def get_ics_directory() -> str | None:
|
||||
"Le répertoire contenant les ics: prend le parent de edt_ics_path"
|
||||
edt_ics_path = ScoDocSiteConfig.get("edt_ics_path")
|
||||
if not edt_ics_path.strip():
|
||||
return None
|
||||
return os.path.split(edt_ics_path)[0]
|
||||
|
||||
|
||||
def get_ics_user_edt_filename(edt_id) -> str | None:
|
||||
"""Le chemin vers le fichier ics de l'emploi du temps de l'utilisateur,
|
||||
ou None.
|
||||
edt_id est l'edt_id de l'utilisateur
|
||||
"""
|
||||
if not edt_id:
|
||||
return None
|
||||
edt_ics_user_path = ScoDocSiteConfig.get("edt_ics_user_path")
|
||||
if not edt_ics_user_path.strip():
|
||||
return None
|
||||
return edt_ics_user_path.format(edt_id=edt_id)
|
||||
|
||||
|
||||
def list_edt_calendars() -> list[str]:
|
||||
"""Liste des chemins complets vers tous les ics des calendriers de semestres"""
|
||||
path = get_ics_directory()
|
||||
return glob.glob(path + "/*.ics") if path else []
|
||||
|
||||
|
||||
def is_edt_configured() -> bool:
|
||||
"True si accès EDT configuré"
|
||||
return bool(ScoDocSiteConfig.get("edt_ics_path"))
|
||||
@ -75,6 +104,16 @@ def formsemestre_load_calendar(
|
||||
# Ne charge qu'un seul ics pour le semestre, prend uniquement
|
||||
# le premier edt_id
|
||||
ics_filename = get_ics_filename(edt_ids[0])
|
||||
return load_calendar(ics_filename, formsemestre=formsemestre, edt_id=edt_id)
|
||||
|
||||
|
||||
def load_calendar(
|
||||
ics_filename: str, formsemestre: FormSemestre | None = None, edt_id: str = None
|
||||
) -> tuple[bytes, icalendar.cal.Calendar]:
|
||||
"""Load ics from given (full) path. Return raw ics and decoded calendar.
|
||||
formsemestre and edt_id are optionaly used for error messages.
|
||||
May raise ScoValueError.
|
||||
"""
|
||||
if ics_filename is None:
|
||||
raise ScoValueError("accès aux emplois du temps non configuré (pas de chemin)")
|
||||
try:
|
||||
@ -235,6 +274,22 @@ def formsemestre_edt_dict(
|
||||
return events_cal
|
||||
|
||||
|
||||
def get_ics_uid_pattern() -> re.Pattern:
|
||||
"""L'expression régulière compilée pour extraire l'identifiant de l'enseignant.
|
||||
May raise ScoValueError.
|
||||
"""
|
||||
edt_ics_uid_regexp = ScoDocSiteConfig.get("edt_ics_uid_regexp")
|
||||
try:
|
||||
edt_ics_uid_pattern = (
|
||||
re.compile(edt_ics_uid_regexp) if edt_ics_uid_regexp else None
|
||||
)
|
||||
except re.error as exc:
|
||||
raise ScoValueError(
|
||||
"expression d'extraction de l'enseignant depuis l'emploi du temps invalide"
|
||||
) from exc
|
||||
return edt_ics_uid_pattern
|
||||
|
||||
|
||||
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
|
||||
"""Chargement fichier ics, filtrage et extraction des identifiants.
|
||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
||||
@ -280,15 +335,8 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
"expression d'extraction du module depuis l'emploi du temps invalide"
|
||||
) from exc
|
||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||
edt_ics_uid_regexp = ScoDocSiteConfig.get("edt_ics_uid_regexp")
|
||||
try:
|
||||
edt_ics_uid_pattern = (
|
||||
re.compile(edt_ics_uid_regexp) if edt_ics_uid_regexp else None
|
||||
)
|
||||
except re.error as exc:
|
||||
raise ScoValueError(
|
||||
"expression d'extraction de l'enseignant depuis l'emploi du temps invalide"
|
||||
) from exc
|
||||
edt_ics_uid_pattern = get_ics_uid_pattern()
|
||||
|
||||
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
group_colors = {
|
||||
@ -390,7 +438,10 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
|
||||
|
||||
def extract_event_data(
|
||||
event: icalendar.cal.Event, ics_field: str, pattern: re.Pattern
|
||||
event: icalendar.cal.Event,
|
||||
ics_field: str,
|
||||
pattern: re.Pattern,
|
||||
none_if_no_match=False,
|
||||
) -> str:
|
||||
"""Extrait la chaine (id) de l'évènement."""
|
||||
if not event.has_key(ics_field):
|
||||
@ -399,8 +450,8 @@ def extract_event_data(
|
||||
m = pattern.search(data)
|
||||
if m and len(m.groups()) > 0:
|
||||
return m.group(1)
|
||||
# fallback: ics field, complete
|
||||
return data
|
||||
# fallback: if not none_if_no_match, ics field complete
|
||||
return None if none_if_no_match else data
|
||||
|
||||
|
||||
def formsemestre_retreive_modimpls_from_edt_id(
|
||||
|
@ -110,6 +110,9 @@ c'est à dire à la montre des étudiants.
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-edt">
|
||||
{{ wtf.form_field(form.edt_ics_user_path) }}
|
||||
</div>
|
||||
<div class="config-section">Extraction des identifiants depuis les calendriers</div>
|
||||
<div class="help">
|
||||
Indiquer ici comment récupérer les informations (titre, groupe, module)
|
||||
|
@ -334,6 +334,7 @@ def config_assiduites():
|
||||
("edt_ics_mod_regexp", "Expression extraction module"),
|
||||
("edt_ics_uid_field", "Champ contenant l'enseignant"),
|
||||
("edt_ics_uid_regexp", "Expression extraction de l'enseignant"),
|
||||
("edt_ics_user_path", "Chemin vers les ics des enseignants"),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.73"
|
||||
SCOVERSION = "9.6.74"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -714,3 +714,11 @@ def downgrade_assiduites_module(
|
||||
):
|
||||
"""Supprime les assiduites et/ou les justificatifs de tous les départements ou du département sélectionné"""
|
||||
tools.downgrade_module(dept, assiduites, justificatifs)
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def generate_ens_calendars(): # generate-ens-calendars
|
||||
"""Génère les calendrier enseignants à partir des ics semestres"""
|
||||
from tools.edt import edt_ens
|
||||
|
||||
edt_ens.generate_ens_calendars()
|
||||
|
1
tools/edt/README.md
Normal file
1
tools/edt/README.md
Normal file
@ -0,0 +1 @@
|
||||
# Outils pour manipuler les emplois du temps ical
|
125
tools/edt/edt_ens.py
Normal file
125
tools/edt/edt_ens.py
Normal file
@ -0,0 +1,125 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 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
|
||||
|
||||
Génération des ics par enseignant à partir des ics par formation.
|
||||
A utiliser quand les edt des enseignants ne sont pas fournis par l'extérieur.
|
||||
|
||||
Utilise la configuration globale pour extraire l'enseignant de chaque évènement.
|
||||
Les évènements sans enseignant identifiable sont ignorés.
|
||||
Tous les ics de `edt_ics_path`/*.ics sont lus.
|
||||
L'edt de chaque enseignant est enregistré dans `edt_ics_user_path`/user_edt_id.ics
|
||||
|
||||
La construction des ics se fait en mémoire, les fichiers sont écrits à la fin.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import time
|
||||
import icalendar
|
||||
|
||||
from flask import flash
|
||||
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import sco_version
|
||||
|
||||
|
||||
def _calendar_factory():
|
||||
"Create a new calendar"
|
||||
cal = icalendar.Calendar()
|
||||
cal.add(
|
||||
"prodid", f"-//{sco_version.SCONAME} {sco_version.SCOVERSION}//scodoc.org//"
|
||||
)
|
||||
cal.add("version", "2.0")
|
||||
return cal
|
||||
|
||||
|
||||
def generate_ens_calendars():
|
||||
"""Regénère les calendriers de tous les enseignants"""
|
||||
t0 = time.time()
|
||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||
if not edt_ics_uid_field:
|
||||
log("generate_ens_calendars: no edt_ics_uid_field, aborting")
|
||||
return
|
||||
edt_ics_user_path = ScoDocSiteConfig.get("edt_ics_user_path")
|
||||
if not edt_ics_user_path:
|
||||
log("generate_ens_calendars: no edt_ics_user_path, aborting")
|
||||
return
|
||||
edt_ics_uid_pattern = sco_edt_cal.get_ics_uid_pattern()
|
||||
edt_by_uid = defaultdict(_calendar_factory)
|
||||
edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache)
|
||||
nb_events = 0 # to log
|
||||
ics_filenames = sco_edt_cal.list_edt_calendars()
|
||||
for ics_filename in ics_filenames:
|
||||
try:
|
||||
_, calendar = sco_edt_cal.load_calendar(ics_filename)
|
||||
except ScoValueError:
|
||||
continue # skip, ignoring errors
|
||||
if not calendar:
|
||||
continue
|
||||
|
||||
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
||||
nb_events += len(events)
|
||||
ens: User | None = None
|
||||
for event in events:
|
||||
edt_ens = sco_edt_cal.extract_event_data(
|
||||
event, edt_ics_uid_field, edt_ics_uid_pattern, none_if_no_match=True
|
||||
)
|
||||
if edt_ens in edt2user:
|
||||
ens = edt2user[edt_ens]
|
||||
else:
|
||||
ens = User.query.filter_by(edt_id=edt_ens).first()
|
||||
edt2user[edt_ens] = ens
|
||||
if ens: # si l'utilisateur est reconnu
|
||||
event.add("X-ScoDoc-user", ens.user_name)
|
||||
edt_by_uid[edt_ens].add_component(event)
|
||||
_write_user_calendars(edt_by_uid)
|
||||
log(
|
||||
f"""generate_ens_calendars: done in {(time.time()-t0):g}s, processed {
|
||||
nb_events} events from {len(ics_filenames)} calendars, for {len(edt_by_uid)} users"""
|
||||
)
|
||||
|
||||
|
||||
def _write_user_calendars(edt_by_uid: defaultdict):
|
||||
"""Write all ics files, one per user"""
|
||||
for edt_id, cal in edt_by_uid.items():
|
||||
if not len(cal): # nb: empty cals are True
|
||||
continue # safeguard, never generate empty cals
|
||||
filename = sco_edt_cal.get_ics_user_edt_filename(edt_id)
|
||||
if filename:
|
||||
log(f"generate_ens_calendars: writing {filename}")
|
||||
try:
|
||||
with open(filename, "wb") as f:
|
||||
f.write(cal.to_ical())
|
||||
except PermissionError:
|
||||
log("_write_user_calendars: permission denied")
|
||||
flash("Erreur: permission écriture calendrier non accordée")
|
||||
return # abort
|
||||
except FileNotFoundError:
|
||||
log("_write_user_calendars: No such file or directory")
|
||||
flash("Erreur: chemin incorrect pour écrire le calendrier")
|
||||
return # abort
|
Loading…
Reference in New Issue
Block a user