diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index b44498007..6bfbbdbb2 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -269,10 +269,12 @@ class FormSemestre(db.Model):
return default_partition.groups.first()
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
- def get_edt_id(self) -> str:
- "l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée"
+ def get_edt_ids(self) -> list[str]:
+ "l'ids pour l'emploi du temps: à défaut, les codes étape Apogée"
return (
- self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "")
+ scu.split_id(self.edt_id)
+ or [e.etape_apo.strip() for e in self.etapes if e.etape_apo]
+ or []
)
def get_infos_dict(self) -> dict:
@@ -1040,6 +1042,33 @@ class FormSemestre(db.Model):
nb_recorded += 1
return nb_recorded
+ def change_formation(self, formation_dest: Formation):
+ """Associe ce formsemestre à une autre formation.
+ Ce n'est possible que si la formation destination possède des modules de
+ même code que ceux utilisés dans la formation d'origine du formsemestre.
+ S'il manque un module, l'opération est annulée.
+ Commit (or rollback) session.
+ """
+ ok = True
+ for mi in self.modimpls:
+ dest_modules = formation_dest.modules.filter_by(code=mi.module.code).all()
+ match len(dest_modules):
+ case 1:
+ mi.module = dest_modules[0]
+ db.session.add(mi)
+ case 0:
+ print(f"Argh ! no module found with code={mi.module.code}")
+ ok = False
+ case _:
+ print(f"Arg ! several modules found with code={mi.module.code}")
+ ok = False
+
+ if ok:
+ self.formation_id = formation_dest.id
+ db.session.commit()
+ else:
+ db.session.rollback()
+
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table(
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index 468888e6a..77eba7da8 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -58,12 +58,12 @@ class ModuleImpl(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x}
return self.module.get_codes_apogee()
- def get_edt_id(self) -> str:
- "l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
+ def get_edt_ids(self) -> list[str]:
+ "les ids pour l'emploi du temps: à défaut, les codes Apogée"
return (
- self.edt_id
- or (self.code_apogee.split(",")[0] if self.code_apogee else "")
- or self.module.get_edt_id()
+ scu.split_id(self.edt_id)
+ or scu.split_id(self.code_apogee)
+ or self.module.get_edt_ids()
)
def get_evaluations_poids(self) -> pd.DataFrame:
diff --git a/app/models/modules.py b/app/models/modules.py
index 5a96de3a9..891a476dc 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -285,13 +285,9 @@ class Module(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x}
return set()
- def get_edt_id(self) -> str:
- "l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
- return (
- self.edt_id
- or (self.code_apogee.split(",")[0] if self.code_apogee else "")
- or ""
- )
+ def get_edt_ids(self) -> list[str]:
+ "les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
+ return scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
def get_parcours(self) -> list[ApcParcours]:
"""Les parcours utilisant ce module.
diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py
index 2327071ba..0857656df 100644
--- a/app/scodoc/sco_edit_formation.py
+++ b/app/scodoc/sco_edit_formation.py
@@ -80,7 +80,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
f"""
Confirmer la suppression de la formation
{formation.titre} ({formation.acronyme}) ?
- Attention: la suppression d'une formation est irréversible
+
Attention: la suppression d'une formation est irréversible
et implique la supression de toutes les UE, matières et modules de la formation !
""",
@@ -273,7 +273,8 @@ def formation_edit(formation_id=None, create=False):
"\n".join(H)
+ tf_error_message(
f"""Valeurs incorrectes: il existe déjà une formation avec même titre,
acronyme et version.
"""
@@ -285,11 +286,11 @@ def formation_edit(formation_id=None, create=False):
if create:
formation = do_formation_create(tf[2])
else:
- do_formation_edit(tf[2])
- flash(
- f"""Création de la formation {
- formation.titre} ({formation.acronyme}) version {formation.version}"""
- )
+ if do_formation_edit(tf[2]):
+ flash(
+ f"""Modification de la formation {
+ formation.titre} ({formation.acronyme}) version {formation.version}"""
+ )
return flask.redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
@@ -335,8 +336,8 @@ def do_formation_create(args: dict) -> Formation:
return formation
-def do_formation_edit(args):
- "edit a formation"
+def do_formation_edit(args) -> bool:
+ "edit a formation, returns True if modified"
# On ne peut jamais supprimer le code formation:
if "formation_code" in args and not args["formation_code"]:
@@ -350,11 +351,16 @@ def do_formation_edit(args):
if "type_parcours" in args:
del args["type_parcours"]
+ modified = False
for field in formation.__dict__:
if field in args:
value = args[field].strip() if isinstance(args[field], str) else args[field]
- if field and field[0] != "_":
+ if field and field[0] != "_" and getattr(formation, field, None) != value:
setattr(formation, field, value)
+ modified = True
+
+ if not modified:
+ return False
db.session.add(formation)
try:
@@ -370,6 +376,7 @@ def do_formation_edit(args):
),
) from exc
formation.invalidate_cached_sems()
+ return True
def module_move(module_id, after=0, redirect=True):
diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py
index 9bfaebf6c..7aebfce7f 100644
--- a/app/scodoc/sco_edt_cal.py
+++ b/app/scodoc/sco_edt_cal.py
@@ -34,7 +34,7 @@ from datetime import timezone
import re
import icalendar
-from flask import flash, g, url_for
+from flask import g, url_for
from app import log
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError
@@ -56,12 +56,14 @@ def formsemestre_load_calendar(
Raises ScoValueError if not configured or not available or invalid format.
"""
if edt_id is None and formsemestre:
- edt_id = formsemestre.get_edt_id()
- if not edt_id:
+ edt_ids = formsemestre.get_edt_ids()
+ if not edt_ids:
raise ScoValueError(
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
)
- ics_filename = get_ics_filename(edt_id)
+ # Ne charge qu'un seul ics pour le semestre, prend uniquement
+ # le premier edt_id
+ ics_filename = get_ics_filename(edt_ids[0])
if ics_filename is None:
raise ScoValueError("accès aux emplois du temps non configuré (pas de chemin)")
try:
@@ -147,40 +149,51 @@ def formsemestre_edt_dict(
if group and group_ids_set and group.id not in group_ids_set:
continue # ignore cet évènement
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']}
"""
+ url_abs = (
+ url_for(
+ "assiduites.signal_assiduites_group",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre.id,
+ group_ids=group.id,
+ heure_deb=event["heure_deb"],
+ heure_fin=event["heure_fin"],
+ moduleimpl_id=modimpl.id,
+ jour=event["jour"],
)
+ if modimpl and group
+ else None
+ )
+ match modimpl:
+ case False: # EDT non configuré
+ mod_disp = f"""{scu.EMO_WARNING} non configuré"""
+ bubble = "extraction emploi du temps non configurée"
+ case None: # Module edt non trouvé dans ScoDoc
+ mod_disp = f"""{
+ scu.EMO_WARNING} {event['edt_module']}"""
+ bubble = "code module non trouvé dans ScoDoc. Vérifier configuration."
+ case _: # module EDT bien retrouvé dans ScoDoc
+ mod_disp = f"""{
+ modimpl.module.code}"""
+ bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})"
+
+ title = f"""
+ """
+
# --- Lien saisie abs
link_abs = (
f""""""
- if modimpl and group
+ if url_abs
else ""
)
d = {
# Champs utilisés par tui.calendar
"calendarId": "cal1",
- "title": event["title"] + group_disp + mod_disp + link_abs,
+ "title": f"""{title} {group_disp} {link_abs}""",
"start": event["start"],
"end": event["end"],
"backgroundColor": event["group_bg_color"],
@@ -245,11 +258,13 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
for event in events:
if "DESCRIPTION" in event:
# --- Titre de l'évènement
- title = (
+ title_edt = (
extract_event_data(event, edt_ics_title_field, edt_ics_title_pattern)
if edt_ics_title_pattern
else "non configuré"
)
+ # title remplacé par le nom du module scodoc quand il est trouvé
+ title = title_edt
# --- Group
if edt_ics_group_pattern:
edt_group = extract_event_data(
@@ -278,6 +293,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
event, edt_ics_mod_field, edt_ics_mod_pattern
)
modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
+ if modimpl:
+ title = modimpl.module.titre_str()
else:
modimpl = False
edt_module = ""
@@ -285,7 +302,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
#
events_sco.append(
{
- "title": title,
+ "title": title, # titre event ou nom module
+ "title_edt": title_edt, # titre event
"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
@@ -376,8 +394,11 @@ 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)
+ edt2modimpl = {}
+ for modimpl in formsemestre.modimpls:
+ for edt_id in modimpl.get_edt_ids():
+ if edt_id:
+ edt2modimpl[edt_id] = modimpl
return edt2modimpl
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index dbd864f53..e17b4945a 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -307,7 +307,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
D = sco_xml.xml_to_dicts(f)
except Exception as exc:
raise ScoFormatError(
- """Ce document xml ne correspond pas à un programme exporté par ScoDoc.
+ """Ce document xml ne correspond pas à un programme exporté par ScoDoc.
(élément 'formation' inexistant par exemple)."""
) from exc
assert D[0] == "formation"
@@ -322,8 +322,13 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_competence_id = _formation_retreive_refcomp(f_dict)
f_dict["referentiel_competence_id"] = referentiel_competence_id
# find new version number
+ acronyme_lower = f_dict["acronyme"].lower() if f_dict["acronyme"] else ""
+ titre_lower = f_dict["titre"].lower() if f_dict["titre"] else ""
formations: list[Formation] = Formation.query.filter_by(
- acronyme=f_dict["acronyme"], titre=f_dict["titre"], dept_id=f_dict["dept_id"]
+ dept_id=f_dict["dept_id"]
+ ).filter(
+ db.func.lower(Formation.acronyme) == acronyme_lower,
+ db.func.lower(Formation.titre) == titre_lower,
)
if formations.count():
version = max(f.version or 0 for f in formations)
@@ -518,6 +523,7 @@ def formation_list_table() -> GenTable:
"_titre_link_class": "stdlink",
"_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0,
+ "commentaire": formation.commentaire or "",
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
@@ -594,10 +600,12 @@ def formation_list_table() -> GenTable:
"formation_code",
"version",
"titre",
+ "commentaire",
"sems_list_txt",
)
titles = {
"buttons": "",
+ "commentaire": "Commentaire",
"acronyme": "Acro.",
"parcours_name": "Type",
"titre": "Titre",
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 6e04677dc..1523c8e82 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -764,13 +764,23 @@ FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]")
ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE)
-def is_valid_code_nip(s):
+def is_valid_code_nip(s: str) -> bool:
"""True si s peut être un code NIP: au moins 6 chiffres décimaux"""
if not s:
return False
return re.match(r"^[0-9]{6,32}$", s)
+def split_id(ident: str) -> list[str]:
+ """ident est une chaine 'X, Y, Z'
+ Renvoie ['X','Y', 'Z']
+ """
+ if ident:
+ ident = ident.strip()
+ return [x.strip() for x in ident.strip().split(",")] if ident else []
+ return []
+
+
def strnone(s):
"convert s to string, '' if s is false"
if s:
diff --git a/app/static/css/edt.css b/app/static/css/edt.css
index f97c91e85..e60408299 100644
--- a/app/static/css/edt.css
+++ b/app/static/css/edt.css
@@ -6,8 +6,17 @@
font-size: 12pt;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
+.module-edt {
+ display: inline;
+}
+.mod-code {
+ font-weight: bold;
+ color: rgb(21, 21, 116);
+ font-size: 110%;
+}
.group-name {
color: rgb(25, 113, 25);
+ display: inline;
}
.group-edt {
color: red;
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index da2dfe36d..0f9072c81 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2319,7 +2319,10 @@ table.formation_list_table td.buttons span.but_placeholder {
}
.formation_list_table td.titre {
- width: 50%;
+ width: 45%;
+}
+.formation_list_table td.commentaire {
+ font-style: italic;
}
.formation_list_table td.sems_list_txt {
diff --git a/sco_version.py b/sco_version.py
index 67d9697b6..e8a195ffc 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.6.56"
+SCOVERSION = "9.6.58"
SCONAME = "ScoDoc"