Merge branch 'master' into export_jury

This commit is contained in:
Jean-Marie Place 2023-11-20 13:49:32 +01:00
commit a2467fd236
10 changed files with 145 additions and 62 deletions

View File

@ -269,10 +269,12 @@ class FormSemestre(db.Model):
return default_partition.groups.first() return default_partition.groups.first()
raise ScoValueError("Le semestre n'a pas de groupe par défaut") raise ScoValueError("Le semestre n'a pas de groupe par défaut")
def get_edt_id(self) -> str: def get_edt_ids(self) -> list[str]:
"l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée" "l'ids pour l'emploi du temps: à défaut, les codes étape Apogée"
return ( 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: def get_infos_dict(self) -> dict:
@ -1040,6 +1042,33 @@ class FormSemestre(db.Model):
nb_recorded += 1 nb_recorded += 1
return nb_recorded 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 # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table( notes_formsemestre_responsables = db.Table(

View File

@ -58,12 +58,12 @@ class ModuleImpl(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x} return {x.strip() for x in self.code_apogee.split(",") if x}
return self.module.get_codes_apogee() return self.module.get_codes_apogee()
def get_edt_id(self) -> str: def get_edt_ids(self) -> list[str]:
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée" "les ids pour l'emploi du temps: à défaut, les codes Apogée"
return ( return (
self.edt_id scu.split_id(self.edt_id)
or (self.code_apogee.split(",")[0] if self.code_apogee else "") or scu.split_id(self.code_apogee)
or self.module.get_edt_id() or self.module.get_edt_ids()
) )
def get_evaluations_poids(self) -> pd.DataFrame: def get_evaluations_poids(self) -> pd.DataFrame:

View File

@ -285,13 +285,9 @@ class Module(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x} return {x.strip() for x in self.code_apogee.split(",") if x}
return set() return set()
def get_edt_id(self) -> str: def get_edt_ids(self) -> list[str]:
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée" "les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
return ( return scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
self.edt_id
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
or ""
)
def get_parcours(self) -> list[ApcParcours]: def get_parcours(self) -> list[ApcParcours]:
"""Les parcours utilisant ce module. """Les parcours utilisant ce module.

View File

@ -273,7 +273,8 @@ def formation_edit(formation_id=None, create=False):
"\n".join(H) "\n".join(H)
+ tf_error_message( + tf_error_message(
f"""Valeurs incorrectes: il existe déjà <a href="{ f"""Valeurs incorrectes: il existe déjà <a href="{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=other_formations[0].id) url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=other_formations[0].id)
}">une formation</a> avec même titre, }">une formation</a> avec même titre,
acronyme et version. acronyme et version.
""" """
@ -285,9 +286,9 @@ def formation_edit(formation_id=None, create=False):
if create: if create:
formation = do_formation_create(tf[2]) formation = do_formation_create(tf[2])
else: else:
do_formation_edit(tf[2]) if do_formation_edit(tf[2]):
flash( flash(
f"""Création de la formation { f"""Modification de la formation {
formation.titre} ({formation.acronyme}) version {formation.version}""" formation.titre} ({formation.acronyme}) version {formation.version}"""
) )
return flask.redirect( return flask.redirect(
@ -335,8 +336,8 @@ def do_formation_create(args: dict) -> Formation:
return formation return formation
def do_formation_edit(args): def do_formation_edit(args) -> bool:
"edit a formation" "edit a formation, returns True if modified"
# On ne peut jamais supprimer le code formation: # On ne peut jamais supprimer le code formation:
if "formation_code" in args and not args["formation_code"]: if "formation_code" in args and not args["formation_code"]:
@ -350,11 +351,16 @@ def do_formation_edit(args):
if "type_parcours" in args: if "type_parcours" in args:
del args["type_parcours"] del args["type_parcours"]
modified = False
for field in formation.__dict__: for field in formation.__dict__:
if field in args: if field in args:
value = args[field].strip() if isinstance(args[field], str) else args[field] 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) setattr(formation, field, value)
modified = True
if not modified:
return False
db.session.add(formation) db.session.add(formation)
try: try:
@ -370,6 +376,7 @@ def do_formation_edit(args):
), ),
) from exc ) from exc
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return True
def module_move(module_id, after=0, redirect=True): def module_move(module_id, after=0, redirect=True):

View File

@ -34,7 +34,7 @@ from datetime import timezone
import re import re
import icalendar import icalendar
from flask import flash, g, url_for from flask import g, url_for
from app import log from app import log
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError 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. Raises ScoValueError if not configured or not available or invalid format.
""" """
if edt_id is None and formsemestre: if edt_id is None and formsemestre:
edt_id = formsemestre.get_edt_id() edt_ids = formsemestre.get_edt_ids()
if not edt_id: if not edt_ids:
raise ScoValueError( raise ScoValueError(
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)" "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: 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:
@ -147,24 +149,9 @@ def formsemestre_edt_dict(
if group and group_ids_set and group.id not in group_ids_set: if group and group_ids_set and group.id not in group_ids_set:
continue # ignore cet évènement continue # ignore cet évènement
modimpl: ModuleImpl | bool = event["modimpl"] modimpl: ModuleImpl | bool = event["modimpl"]
if modimpl is False: url_abs = (
mod_disp = f"""<div class="module-edt" url_for(
title="extraction emploi du temps non configurée"> "assiduites.signal_assiduites_group",
{scu.EMO_WARNING} non configuré
</div>"""
else:
mod_disp = (
f"""<div class="module-edt mod-name" title="{modimpl.module.abbrev or ""}">{
modimpl.module.code}</div>"""
if modimpl
else f"""<div class="module-edt mod-etd" title="code module non trouvé dans ScoDoc.
Vérifier configuration.">{
scu.EMO_WARNING} {event['edt_module']}</div>"""
)
# --- Lien saisie abs
link_abs = (
f"""<div class="module-edt link-abs"><a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
group_ids=group.id, group_ids=group.id,
@ -172,15 +159,41 @@ def formsemestre_edt_dict(
heure_fin=event["heure_fin"], heure_fin=event["heure_fin"],
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
jour=event["jour"], jour=event["jour"],
)}">absences</a> )
</div>"""
if modimpl and group if modimpl and group
else None
)
match modimpl:
case False: # EDT non configuré
mod_disp = f"""<span>{scu.EMO_WARNING} non configuré</span>"""
bubble = "extraction emploi du temps non configurée"
case None: # Module edt non trouvé dans ScoDoc
mod_disp = f"""<span class="mod-etd">{
scu.EMO_WARNING} {event['edt_module']}</span>"""
bubble = "code module non trouvé dans ScoDoc. Vérifier configuration."
case _: # module EDT bien retrouvé dans ScoDoc
mod_disp = f"""<span class="mod-name mod-code" title="{
modimpl.module.abbrev or ""} ({event['edt_module']})">{
modimpl.module.code}</span>"""
bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})"
title = f"""<div class = "module-edt" title="{bubble} {event['title_edt']}">
<a class="discretelink" href="{url_abs or ''}">{mod_disp} <span>{event['title']}</span></a>
</div>
"""
# --- Lien saisie abs
link_abs = (
f"""<div class="module-edt link-abs"><a class="stdlink" href="{
url_abs}">absences</a>
</div>"""
if url_abs
else "" else ""
) )
d = { d = {
# Champs utilisés par tui.calendar # Champs utilisés par tui.calendar
"calendarId": "cal1", "calendarId": "cal1",
"title": event["title"] + group_disp + mod_disp + link_abs, "title": f"""{title} {group_disp} {link_abs}""",
"start": event["start"], "start": event["start"],
"end": event["end"], "end": event["end"],
"backgroundColor": event["group_bg_color"], "backgroundColor": event["group_bg_color"],
@ -245,11 +258,13 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
for event in events: for event in events:
if "DESCRIPTION" in event: if "DESCRIPTION" in event:
# --- Titre de l'évènement # --- Titre de l'évènement
title = ( title_edt = (
extract_event_data(event, edt_ics_title_field, edt_ics_title_pattern) extract_event_data(event, edt_ics_title_field, edt_ics_title_pattern)
if edt_ics_title_pattern if edt_ics_title_pattern
else "non configuré" else "non configuré"
) )
# title remplacé par le nom du module scodoc quand il est trouvé
title = title_edt
# --- Group # --- Group
if edt_ics_group_pattern: if edt_ics_group_pattern:
edt_group = extract_event_data( 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 event, edt_ics_mod_field, edt_ics_mod_pattern
) )
modimpl: ModuleImpl = edt2modimpl.get(edt_module, None) modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
if modimpl:
title = modimpl.module.titre_str()
else: else:
modimpl = False modimpl = False
edt_module = "" edt_module = ""
@ -285,7 +302,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
# #
events_sco.append( 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 "edt_group": edt_group, # id group edt non traduit
"group": group, # False si extracteur non configuré "group": group, # False si extracteur non configuré
"group_bg_color": group_bg_color, # associée au groupe "group_bg_color": group_bg_color, # associée au groupe
@ -376,8 +394,11 @@ def formsemestre_retreive_modimpls_from_edt_id(
formsemestre: FormSemestre, formsemestre: FormSemestre,
) -> dict[str, ModuleImpl]: ) -> dict[str, ModuleImpl]:
"""Construit un dict donnant le moduleimpl de chaque edt_id""" """Construit un dict donnant le moduleimpl de chaque edt_id"""
edt2modimpl = {modimpl.get_edt_id(): modimpl for modimpl in formsemestre.modimpls} edt2modimpl = {}
edt2modimpl.pop("", None) for modimpl in formsemestre.modimpls:
for edt_id in modimpl.get_edt_ids():
if edt_id:
edt2modimpl[edt_id] = modimpl
return edt2modimpl return edt2modimpl

View File

@ -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) referentiel_competence_id = _formation_retreive_refcomp(f_dict)
f_dict["referentiel_competence_id"] = referentiel_competence_id f_dict["referentiel_competence_id"] = referentiel_competence_id
# find new version number # 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( 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(): if formations.count():
version = max(f.version or 0 for f in formations) version = max(f.version or 0 for f in formations)
@ -518,6 +523,7 @@ def formation_list_table() -> GenTable:
"_titre_link_class": "stdlink", "_titre_link_class": "stdlink",
"_titre_id": f"""titre-{acronyme_no_spaces}""", "_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0, "version": formation.version or 0,
"commentaire": formation.commentaire or "",
} }
# Ajoute les semestres associés à chaque formation: # Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by( row["formsemestres"] = formation.formsemestres.order_by(
@ -594,10 +600,12 @@ def formation_list_table() -> GenTable:
"formation_code", "formation_code",
"version", "version",
"titre", "titre",
"commentaire",
"sems_list_txt", "sems_list_txt",
) )
titles = { titles = {
"buttons": "", "buttons": "",
"commentaire": "Commentaire",
"acronyme": "Acro.", "acronyme": "Acro.",
"parcours_name": "Type", "parcours_name": "Type",
"titre": "Titre", "titre": "Titre",

View File

@ -764,13 +764,23 @@ FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]")
ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) 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""" """True si s peut être un code NIP: au moins 6 chiffres décimaux"""
if not s: if not s:
return False return False
return re.match(r"^[0-9]{6,32}$", s) 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): def strnone(s):
"convert s to string, '' if s is false" "convert s to string, '' if s is false"
if s: if s:

View File

@ -6,8 +6,17 @@
font-size: 12pt; font-size: 12pt;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 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 { .group-name {
color: rgb(25, 113, 25); color: rgb(25, 113, 25);
display: inline;
} }
.group-edt { .group-edt {
color: red; color: red;

View File

@ -2319,7 +2319,10 @@ table.formation_list_table td.buttons span.but_placeholder {
} }
.formation_list_table td.titre { .formation_list_table td.titre {
width: 50%; width: 45%;
}
.formation_list_table td.commentaire {
font-style: italic;
} }
.formation_list_table td.sems_list_txt { .formation_list_table td.sems_list_txt {

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.56" SCOVERSION = "9.6.58"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"