/edt
TODO: spécifier intervalle de dates start et end
- TODO: cacher ?
"""
- # Correspondances id edt -> id scodoc pour groupes, modules et enseignants
+ events_scodoc = _load_and_convert_ics(formsemestre)
+ # Génération des événements pour le calendrier html
+ events_cal = []
+ for event in events_scodoc:
+ group: GroupDescr | bool = event["group"]
+ if group is False:
+ group_disp = f"""
+
+ {scu.EMO_WARNING} non configuré
+
"""
+ else:
+ group_disp = (
+ f"""{group.get_nom_with_part(default="promo")}
"""
+ if group
+ else f"""{event['edt_group']}
+
+ {scu.EMO_WARNING} non reconnu
+
"""
+ )
+ modimpl: ModuleImpl | bool = event["modimpl"]
+ if modimpl is False:
+ mod_disp = f"""
+ {scu.EMO_WARNING} non configuré
+
"""
+ else:
+ mod_disp = (
+ f"""{
+ modimpl.module.code}
"""
+ if modimpl
+ else f"""{
+ scu.EMO_WARNING} {event['edt_module']}
"""
+ )
+ # --- Lien saisie abs
+ link_abs = (
+ f""""""
+ if modimpl and group
+ else ""
+ )
+ d = {
+ # Champs utilisés par tui.calendar
+ "calendarId": "cal1",
+ "title": event["title"] + group_disp + mod_disp + link_abs,
+ "start": event["start"],
+ "end": event["end"],
+ "backgroundColor": event["group_bg_color"],
+ # Infos brutes pour usage API éventuel
+ "group_id": group.id if group else None,
+ "group_edt_id": event["edt_group"],
+ "moduleimpl_id": modimpl.id if modimpl else None,
+ }
+ events_cal.append(d)
+
+ return events_cal
+
+
+def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
+ "chargement fichier, filtrage et extraction des identifiants."
+ # Chargement du calendier ics
+ calendar = formsemestre_load_calendar(formsemestre)
+ if not calendar:
+ return []
+ # --- Paramètres d'extraction
+ edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
+ edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp")
+ try:
+ edt_ics_title_pattern = (
+ re.compile(edt_ics_title_regexp) if edt_ics_title_regexp else None
+ )
+ except re.error as exc:
+ raise ScoValueError(
+ "expression d'extraction du titre depuis l'emploi du temps invalide"
+ ) from exc
+ edt_ics_group_field = ScoDocSiteConfig.get("edt_ics_group_field")
+ edt_ics_group_regexp = ScoDocSiteConfig.get("edt_ics_group_regexp")
+ try:
+ edt_ics_group_pattern = (
+ re.compile(edt_ics_group_regexp) if edt_ics_group_regexp else None
+ )
+ except re.error as exc:
+ raise ScoValueError(
+ "expression d'extraction du groupe depuis l'emploi du temps invalide"
+ ) from exc
+ edt_ics_mod_field = ScoDocSiteConfig.get("edt_ics_mod_field")
+ edt_ics_mod_regexp = ScoDocSiteConfig.get("edt_ics_mod_regexp")
+ try:
+ edt_ics_mod_pattern = (
+ re.compile(edt_ics_mod_regexp) if edt_ics_mod_regexp else None
+ )
+ except re.error as exc:
+ raise ScoValueError(
+ "expression d'extraction du module depuis l'emploi du temps invalide"
+ ) from exc
+ # --- 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]
@@ -93,125 +198,129 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
}
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 = []
+ events_sco = []
for event in events:
if "DESCRIPTION" in event:
+ # --- Titre de l'évènement
+ title = (
+ extract_event_data(event, edt_ics_title_field, edt_ics_title_pattern)
+ if edt_ics_title_pattern
+ else "non configuré"
+ )
# --- 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
-
"""
- )
+ if edt_ics_group_pattern:
+ edt_group = extract_event_data(
+ event, edt_ics_group_field, edt_ics_group_pattern
+ )
+ # si pas de groupe dans l'event, oi si groupe non reconnu, prend toute la promo ("tous")
+ group: GroupDescr = (
+ edt2group.get(edt_group, default_group)
+ if edt_group
+ else default_group
+ )
+ group_bg_color = (
+ group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
+ if group
+ else "lightgrey"
+ )
+ else:
+ edt_group = ""
+ group = False
+ group_bg_color = _EVENT_DEFAULT_COLOR
+
# --- 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}
"""
+ if edt_ics_mod_pattern:
+ edt_module = extract_event_data(
+ event, edt_ics_mod_field, edt_ics_mod_pattern
+ )
+ modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
+ else:
+ modimpl = False
+ edt_module = ""
+ # --- TODO: enseignant
+ #
+ events_sco.append(
+ {
+ "title": title,
+ "edt_group": edt_group, # id group edt non traduit
+ "group": group, # False si extracteur non configuré
+ "group_bg_color": group_bg_color, # associée au groupe
+ "modimpl": modimpl, # False si extracteur non configuré
+ "edt_module": edt_module, # id module edt non traduit
+ "heure_deb": event.decoded("dtstart").strftime("%H:%M"),
+ "heure_fin": event.decoded("dtend").strftime("%H:%M"),
+ "jour": event.decoded("dtstart").isoformat(),
+ "start": event.decoded("dtstart").isoformat(),
+ "end": event.decoded("dtend").isoformat(),
+ }
)
- # --- Lien saisie abs
- link_abs = (
- f""""""
- if modimpl and group
- else ""
- )
- d = {
- # Champs utilisés par tui.calendar
- "calendarId": "cal1",
- "title": extract_event_title(event) + group_disp + mod_disp + link_abs,
- "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
+ return events_sco
-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"):
+def extract_event_data(
+ event: icalendar.cal.Event, ics_field: str, pattern: re.Pattern
+) -> str:
+ """Extrait la chaine (id) de l'évènement."""
+ if not event.has_key(ics_field):
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)
+ data = event.decoded(ics_field).decode("utf-8") # assume ics in utf8
+ m = pattern.search(data)
if m and len(m.groups()) > 0:
return m.group(1)
- # fallback: full description
- return description
+ # fallback: ics field, complete
+ return data
-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_title(event: icalendar.cal.Event) -> str:
+# """Extrait le titre à afficher dans nos calendriers.
+# 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_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 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."""
+# # 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(
diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2
index eb6bec5676..7522968a9c 100644
--- a/app/templates/assiduites/pages/config_assiduites.j2
+++ b/app/templates/assiduites/pages/config_assiduites.j2
@@ -1,6 +1,19 @@
{% extends "base.j2" %}
{% import 'bootstrap/wtf.html' as wtf %}
+{% block styles %}
+{{super()}}
+
+{% endblock %}
+
+
{% block app_content %}
@@ -33,6 +46,25 @@ affectent notamment les comptages d'absences de tous les bulletins des
{{ wtf.form_field(form.edt_ics_path) }}
+ Extraction des identifiants depuis les calendriers
+
+ Indiquer ici comment récupérer les informations (titre, groupe, module)
+ dans les calendriers publiés par votre logiciel d'emploi du temps.
+
+
+ {{ wtf.form_field(form.edt_ics_title_field) }}
+ {{ wtf.form_field(form.edt_ics_title_regexp) }}
+
+
+ {{ wtf.form_field(form.edt_ics_group_field) }}
+ {{ wtf.form_field(form.edt_ics_group_regexp) }}
+
+
+ {{ wtf.form_field(form.edt_ics_mod_field) }}
+ {{ wtf.form_field(form.edt_ics_mod_regexp) }}
+
+
+
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
@@ -41,8 +73,4 @@ affectent notamment les comptages d'absences de tous les bulletins des
-
-
-
-
{% endblock %}
diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2
index b6ee3ad901..2dbdaadb1c 100644
--- a/app/templates/formsemestre/edt.j2
+++ b/app/templates/formsemestre/edt.j2
@@ -51,6 +51,13 @@ document.addEventListener('DOMContentLoaded', function() {
return `${start} ${event.title}`;
},
},
+ timezone: {
+ zones: [
+ {
+ timezoneName: 'CET', // TODO récupérer timezone serveur
+ },
+ ],
+ },
usageStatistics: false,
week: {
dayNames: [ "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
diff --git a/app/views/scodoc.py b/app/views/scodoc.py
index 4ecd9f6077..4d5af816f9 100644
--- a/app/views/scodoc.py
+++ b/app/views/scodoc.py
@@ -324,6 +324,16 @@ def config_assiduites():
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
+ edt_options = (
+ ("edt_ics_path", "Chemin vers les calendriers ics"),
+ ("edt_ics_title_field", "Champ contenant titre"),
+ ("edt_ics_title_regexp", "Expression extraction titre"),
+ ("edt_ics_group_field", "Champ contenant groupe"),
+ ("edt_ics_group_regexp", "Expression extraction groupe"),
+ ("edt_ics_mod_field", "Champ contenant module"),
+ ("edt_ics_mod_regexp", "Expression extraction module"),
+ )
+
if form.validate_on_submit():
if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]):
flash("Heure du début de la journée enregistrée")
@@ -333,8 +343,11 @@ def config_assiduites():
flash("Heure de fin de la journée enregistrée")
if ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"])):
flash("Granularité de la timeline enregistrée")
- if ScoDocSiteConfig.set("edt_ics_path", form.data["edt_ics_path"]):
- flash("Chemin vers les calendriers ics enregistré")
+ # --- Calendriers emploi du temps
+ for opt_name, message in edt_options:
+ if ScoDocSiteConfig.set(opt_name, form.data[opt_name]):
+ flash(f"{message} enregistré")
+
return redirect(url_for("scodoc.configuration"))
if request.method == "GET":
@@ -352,7 +365,9 @@ def config_assiduites():
except ValueError:
form.tick_time.data = 15.0
ScoDocSiteConfig.set("assi_tick_time", 15.0)
- form.edt_ics_path.data = ScoDocSiteConfig.get("edt_ics_path")
+ # --- Emplois du temps
+ for opt_name, _ in edt_options:
+ getattr(form, opt_name).data = ScoDocSiteConfig.get(opt_name)
return render_template(
"assiduites/pages/config_assiduites.j2",