ScoDoc/tests/unit/yaml_setup.py

362 lines
12 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
Met en place une base pour les tests unitaires, à partir d'une description
YAML qui peut donner la formation, son ref. compétences, les formsemestres,
les étudiants et leurs notes et décisions de jury.
Le traitement est effectué dans l'ordre suivant:
setup_from_yaml()
- setup_formation():
- import de la formation (le test utilise une seule formation)
- associe_ues_et_parcours():
- crée les associations formation <-> référentiel de compétence
- setup_formsemestre()
- crée les formsemestres décrits dans le YAML
avec tous les modules du semestre ou des parcours si indiqués
et une évaluation dans chaque moduleimpl.
- inscrit_les_etudiants()
- inscrit et place dans les groupes de parcours
- note_les_modules()
- saisie de toutes les notes indiquées dans le YAML
check_deca_fields()
- vérifie les champs du deca (nb UE, compétences, ...) mais pas les décisions de jury.
formsemestre_validation_auto_but(only_adm=False)
- enregistre toutes les décisions "par défaut" proposées (pas seulement les ADM)
test_but_jury()
- compare décisions attendues indiquées dans le YAML avec celles de ScoDoc
et enregistre immédiatement APRES la décision manuelle indiquée par `decision_jury`
dans le YAML.
Les tests unitaires associés sont généralement lents (construction de la base),
et donc marqués par `@pytest.mark.slow`.
"""
import os
import yaml
from flask import g
from app import db
from app.auth.models import User
from app.models import (
ApcParcours,
DispenseUE,
Evaluation,
Formation,
FormSemestre,
Identite,
Module,
ModuleImpl,
UniteEns,
)
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_utils as scu
from tests.conftest import RESOURCES_DIR
from tests.unit import yaml_setup_but
def setup_formation(formation_infos: dict) -> Formation:
"""Importe la formation, qui est lue à partir du fichier XML
formation_infos["filename"].
La formation peut être quelconque, on vérifie juste qu'elle est bien créée.
"""
# Lecture fichier XML local:
with open(
os.path.join(RESOURCES_DIR, "formations", formation_infos["filename"]),
encoding="utf-8",
) as f:
doc = f.read()
# --- Création de la formation
formation_id, _, _ = sco_formations.formation_import_xml(doc)
formation: Formation = db.session.get(Formation, formation_id)
assert formation
return formation
def create_formsemestre(
formation: Formation,
parcours: list[ApcParcours],
semestre_id: int,
titre: str,
date_debut: str,
date_fin: str,
elt_sem_apo: str = None,
elt_annee_apo: str = None,
etape_apo: str = None,
) -> FormSemestre:
"Création d'un formsemestre, avec ses modimpls et évaluations"
assert formation.is_apc() or not parcours # parcours seulement si APC
formsemestre = FormSemestre(
formation=formation,
parcours=parcours,
dept_id=g.scodoc_dept_id,
titre=titre,
semestre_id=semestre_id,
date_debut=date_debut,
date_fin=date_fin,
elt_sem_apo=elt_sem_apo,
elt_annee_apo=elt_annee_apo,
)
db.session.add(formsemestre)
db.session.flush()
# set responsable (list)
a_user = User.query.first()
formsemestre.responsables = [a_user]
formsemestre.add_etape(etape_apo)
# Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués
sem_parcours_ids = {p.id for p in parcours}
modules = [
m
for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id)
if not m.parcours # module de tronc commun
or (not sem_parcours_ids) # semestre sans parcours => tous
or ({p.id for p in m.parcours} & sem_parcours_ids)
]
for module in modules:
modimpl = ModuleImpl(module=module, responsable_id=a_user.id)
db.session.add(modimpl)
formsemestre.modimpls.append(modimpl)
# Crée une évaluation dans chaque module
create_evaluations(formsemestre)
# Partition par défaut:
db.session.commit()
partition_id = sco_groups.partition_create(
formsemestre.id, default=True, redirect=False
)
_ = sco_groups.create_group(partition_id, default=True)
# Partition de parcours:
if formsemestre.formation.is_apc():
formsemestre.setup_parcours_groups()
return formsemestre
def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
"""Crée une évaluation dans chaque module du semestre"""
for modimpl in formsemestre.modimpls:
evaluation = Evaluation(
moduleimpl=modimpl,
date_debut=formsemestre.date_debut,
description=f"Exam {modimpl.module.titre}",
coefficient=1.0,
note_max=20.0,
numero=1,
publish_incomplete=publish_incomplete,
)
db.session.add(evaluation)
if formsemestre.formation.is_apc():
# Affecte les mêmes poids que les coefs APC:
ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef }
evaluation.set_ue_poids_dict(ue_coef_dict)
def note_les_modules(doc: dict, formsemestre_titre: str = ""):
"""Saisie les notes des étudiants pour ce formsemestre
doc : données YAML
"""
a_user = User.query.first()
formsemestre: FormSemestre = FormSemestre.query.filter_by(
titre=formsemestre_titre
).first()
assert formsemestre is not None
for nom, infos in doc["Etudiants"].items():
etud: Identite = Identite.query.filter_by(nom=nom).first()
assert etud is not None
sem_infos = infos["formsemestres"].get(formsemestre_titre)
if sem_infos:
for code_module, note in sem_infos.get("notes_modules", {}).items():
modimpl = (
formsemestre.modimpls.join(Module)
.filter_by(code=code_module)
.first()
)
# le sem. doit avoir un module du code indiqué:
if modimpl is None:
raise ValueError(f"module de code '{code_module}' introuvable")
for evaluation in modimpl.evaluations:
# s'il y a plusieurs evals, affecte la même note à chacune
note_value, invalid = sco_saisie_notes.convert_note_from_string(
str(note),
20.0,
note_min=scu.NOTES_MIN,
etudid=etud.id,
absents=[],
tosuppress=[],
invalids=[],
)
assert not invalid # valeur note invalide
assert isinstance(note_value, float)
sco_saisie_notes.notes_add(
a_user,
evaluation.id,
[(etud.id, note_value)],
comment="note_les_modules",
)
def setup_formsemestre(
formation: Formation, doc: str, formsemestre_titre: str = ""
) -> FormSemestre:
"""Création du formsemestre de titre indiqué.
Le cas échéant associé à son parcours.
"""
infos = doc["FormSemestres"][formsemestre_titre]
codes_parcours = infos.get("codes_parcours", [])
assert formation.is_apc() or not codes_parcours # parcours seulement en APC
parcours = []
for code_parcour in codes_parcours:
parcour = formation.referentiel_competence.parcours.filter_by(
code=code_parcour
).first()
assert parcour is not None
parcours.append(parcour)
elt_sem_apo = infos.get("elt_sem_apo")
elt_annee_apo = infos.get("elt_annee_apo")
etape_apo = infos.get("etape_apo")
formsemestre = create_formsemestre(
formation,
parcours,
infos["idx"],
formsemestre_titre,
infos["date_debut"],
infos["date_fin"],
elt_sem_apo=elt_sem_apo,
elt_annee_apo=elt_annee_apo,
etape_apo=etape_apo,
)
db.session.flush()
return formsemestre
def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
"""Inscrit les étudiants dans chacun de leurs formsemestres"""
etudiants = doc.get("Etudiants")
if not etudiants:
return
formsemestre: FormSemestre = FormSemestre.query.filter_by(
titre=formsemestre_titre
).first()
assert formsemestre is not None
partition_parcours = formsemestre.partitions.filter_by(
partition_name=scu.PARTITION_PARCOURS
).first()
for nom, infos in etudiants.items():
# Création des étudiants (sauf si déjà existants)
prenom = infos.get("prenom", "prénom")
civilite = infos.get("civilite", "X")
code_nip = infos.get("code_nip", None)
etud = Identite.query.filter_by(
nom=nom, prenom=prenom, civilite=civilite
).first()
if etud is None:
etud = Identite.create_etud(
dept_id=g.scodoc_dept_id,
nom=nom,
prenom=prenom,
civilite=civilite,
code_nip=code_nip,
)
db.session.add(etud)
db.session.commit()
# L'inscrire au formsemestre
sem_infos = infos["formsemestres"].get(formsemestre_titre)
if sem_infos:
if partition_parcours is not None and "parcours" in sem_infos:
group = partition_parcours.groups.filter_by(
group_name=sem_infos["parcours"]
).first()
assert group is not None # le groupe de parcours doit exister
group_ids = [group.id]
else:
group_ids = []
# Génère des dispenses d'UEs
if "dispense_ues" in sem_infos:
etud_dispense_ues(formsemestre, etud, sem_infos["dispense_ues"])
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
formsemestre.id,
etud.id,
group_ids=group_ids,
etat=scu.INSCRIT,
method="tests/unit/inscrit_les_etudiants",
)
# Met à jour les inscriptions:
formsemestre.update_inscriptions_parcours_from_groups()
def etud_dispense_ues(
formsemestre: FormSemestre, etud: Identite, ue_acronymes: list[str]
):
"""Génère des dispenses d'UE"""
for ue_acronyme in set(ue_acronymes):
ue: UniteEns = formsemestre.formation.ues.filter_by(
acronyme=ue_acronyme
).first()
assert ue
disp = DispenseUE(formsemestre_id=formsemestre.id, ue_id=ue.id, etudid=etud.id)
db.session.add(disp)
def setup_from_yaml(filename: str) -> tuple[dict, Formation, list[str]]:
"""Lit le fichier yaml et construit l'ensemble des objets"""
with open(filename, encoding="utf-8") as f:
doc = yaml.safe_load(f.read())
# Passe tous les noms en majuscules (car stockés ainsi en base)
if "Etudiants" in doc:
doc["Etudiants"] = {k.upper(): v for (k, v) in doc["Etudiants"].items()}
for e in doc["Etudiants"].values():
if "prenom" in e:
e["prenom"] = e["prenom"].upper()
# Charge de ref. comp. avant la formation, de façon à pouvoir
# re-créer les associations UE/Niveaux
yaml_setup_but.setup_formation_referentiel(doc.get("ReferentielCompetences", {}))
formation = setup_formation(doc["Formation"])
yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])
formsemestre_titres = list(doc["FormSemestres"].keys())
return doc, formation, formsemestre_titres
def create_formsemestre_with_etuds(
doc: dict, formation: Formation, formsemestre_titre: str
) -> FormSemestre:
"""Création formsemestre de titre indiqué, puis inscrit ses étudianst et les note"""
formsemestre = setup_formsemestre(
formation, doc, formsemestre_titre=formsemestre_titre
)
etudiants = doc.get("Etudiants")
if etudiants:
inscrit_les_etudiants(doc, formsemestre_titre=formsemestre_titre)
note_les_modules(doc, formsemestre_titre=formsemestre_titre)
return formsemestre