forked from ScoDoc/ScoDoc
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.""",
|
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
||||||
validators=[Optional(), check_ics_path],
|
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(
|
edt_ics_title_field = StringField(
|
||||||
label="Champ contenant le titre",
|
label="Champ contenant le titre",
|
||||||
|
@ -31,6 +31,8 @@ Lecture et conversion des ics.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import icalendar
|
import icalendar
|
||||||
@ -51,6 +53,33 @@ def get_ics_filename(edt_id: str) -> str | None:
|
|||||||
return edt_ics_path.format(edt_id=edt_id)
|
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:
|
def is_edt_configured() -> bool:
|
||||||
"True si accès EDT configuré"
|
"True si accès EDT configuré"
|
||||||
return bool(ScoDocSiteConfig.get("edt_ics_path"))
|
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
|
# Ne charge qu'un seul ics pour le semestre, prend uniquement
|
||||||
# le premier edt_id
|
# le premier edt_id
|
||||||
ics_filename = get_ics_filename(edt_ids[0])
|
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:
|
if ics_filename is None:
|
||||||
raise ScoValueError("accès aux emplois du temps non configuré (pas de chemin)")
|
raise ScoValueError("accès aux emplois du temps non configuré (pas de chemin)")
|
||||||
try:
|
try:
|
||||||
@ -235,6 +274,22 @@ def formsemestre_edt_dict(
|
|||||||
return events_cal
|
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]]:
|
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
|
||||||
"""Chargement fichier ics, filtrage et extraction des identifiants.
|
"""Chargement fichier ics, filtrage et extraction des identifiants.
|
||||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
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"
|
"expression d'extraction du module depuis l'emploi du temps invalide"
|
||||||
) from exc
|
) from exc
|
||||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||||
edt_ics_uid_regexp = ScoDocSiteConfig.get("edt_ics_uid_regexp")
|
edt_ics_uid_pattern = get_ics_uid_pattern()
|
||||||
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
|
|
||||||
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||||
group_colors = {
|
group_colors = {
|
||||||
@ -390,7 +438,10 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
|||||||
|
|
||||||
|
|
||||||
def extract_event_data(
|
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:
|
) -> str:
|
||||||
"""Extrait la chaine (id) de l'évènement."""
|
"""Extrait la chaine (id) de l'évènement."""
|
||||||
if not event.has_key(ics_field):
|
if not event.has_key(ics_field):
|
||||||
@ -399,8 +450,8 @@ def extract_event_data(
|
|||||||
m = pattern.search(data)
|
m = pattern.search(data)
|
||||||
if m and len(m.groups()) > 0:
|
if m and len(m.groups()) > 0:
|
||||||
return m.group(1)
|
return m.group(1)
|
||||||
# fallback: ics field, complete
|
# fallback: if not none_if_no_match, ics field complete
|
||||||
return data
|
return None if none_if_no_match else data
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_retreive_modimpls_from_edt_id(
|
def formsemestre_retreive_modimpls_from_edt_id(
|
||||||
|
@ -110,6 +110,9 @@ c'est à dire à la montre des étudiants.
|
|||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
</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="config-section">Extraction des identifiants depuis les calendriers</div>
|
||||||
<div class="help">
|
<div class="help">
|
||||||
Indiquer ici comment récupérer les informations (titre, groupe, module)
|
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_mod_regexp", "Expression extraction module"),
|
||||||
("edt_ics_uid_field", "Champ contenant l'enseignant"),
|
("edt_ics_uid_field", "Champ contenant l'enseignant"),
|
||||||
("edt_ics_uid_regexp", "Expression extraction de 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():
|
if form.validate_on_submit():
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.73"
|
SCOVERSION = "9.6.74"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
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é"""
|
"""Supprime les assiduites et/ou les justificatifs de tous les départements ou du département sélectionné"""
|
||||||
tools.downgrade_module(dept, assiduites, justificatifs)
|
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