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()
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(

View File

@ -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:

View File

@ -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.

View File

@ -80,7 +80,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
f"""<h2>Confirmer la suppression de la formation
{formation.titre} ({formation.acronyme}) ?
</h2>
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b>
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b>
et implique la supression de toutes les UE, matières et modules de la formation !
</p>
""",
@ -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à <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,
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):

View File

@ -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"""<div class="module-edt"
title="extraction emploi du temps non configurée">
{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>"""
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"""<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_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"],
)}">absences</a>
url_abs}">absences</a>
</div>"""
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

View File

@ -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",

View File

@ -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:

View File

@ -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;

View File

@ -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 {

View File

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