forked from ScoDoc/ScoDoc
Tests YAML: séparation fct spécifiques BUT
This commit is contained in:
parent
b089773766
commit
e11b7fa076
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.4.40"
|
SCOVERSION = "9.4.41"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ pytest --pdb -m lyon tests/unit/test_but_jury.py
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from tests.unit import yaml_setup
|
from tests.unit import yaml_setup, yaml_setup_but
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
|
from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
|
||||||
@ -46,7 +46,7 @@ def test_but_jury_GB(test_client):
|
|||||||
|
|
||||||
# Vérifie les deca de tous les semestres:
|
# Vérifie les deca de tous les semestres:
|
||||||
for formsemestre in FormSemestre.query:
|
for formsemestre in FormSemestre.query:
|
||||||
yaml_setup.check_deca_fields(formsemestre)
|
yaml_setup_but.check_deca_fields(formsemestre)
|
||||||
|
|
||||||
# Saisie de toutes les décisions de jury
|
# Saisie de toutes les décisions de jury
|
||||||
for formsemestre in FormSemestre.query.order_by(FormSemestre.semestre_id):
|
for formsemestre in FormSemestre.query.order_by(FormSemestre.semestre_id):
|
||||||
@ -54,11 +54,11 @@ def test_but_jury_GB(test_client):
|
|||||||
|
|
||||||
# Vérifie résultats attendus:
|
# Vérifie résultats attendus:
|
||||||
S1: FormSemestre = FormSemestre.query.filter_by(titre="S1_SEE").first()
|
S1: FormSemestre = FormSemestre.query.filter_by(titre="S1_SEE").first()
|
||||||
yaml_setup.test_but_jury(S1, doc)
|
yaml_setup_but.but_test_jury(S1, doc)
|
||||||
S2: FormSemestre = FormSemestre.query.filter_by(titre="S2_SEE").first()
|
S2: FormSemestre = FormSemestre.query.filter_by(titre="S2_SEE").first()
|
||||||
yaml_setup.test_but_jury(S2, doc)
|
yaml_setup_but.but_test_jury(S2, doc)
|
||||||
S3: FormSemestre = FormSemestre.query.filter_by(titre="S3").first()
|
S3: FormSemestre = FormSemestre.query.filter_by(titre="S3").first()
|
||||||
yaml_setup.test_but_jury(S3, doc)
|
yaml_setup_but.but_test_jury(S3, doc)
|
||||||
# _test_but_jury(S1_redoublant, doc)
|
# _test_but_jury(S1_redoublant, doc)
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ def test_but_jury_GMP_lm(test_client):
|
|||||||
|
|
||||||
# Vérifie les deca de tous les semestres:
|
# Vérifie les deca de tous les semestres:
|
||||||
for formsemestre in formsemestres:
|
for formsemestre in formsemestres:
|
||||||
yaml_setup.check_deca_fields(formsemestre)
|
yaml_setup_but.check_deca_fields(formsemestre)
|
||||||
|
|
||||||
# Saisie de toutes les décisions de jury qui ne le seraient pas déjà
|
# Saisie de toutes les décisions de jury qui ne le seraient pas déjà
|
||||||
for formsemestre in formsemestres:
|
for formsemestre in formsemestres:
|
||||||
@ -85,7 +85,7 @@ def test_but_jury_GMP_lm(test_client):
|
|||||||
|
|
||||||
# Vérifie résultats attendus:
|
# Vérifie résultats attendus:
|
||||||
for formsemestre in formsemestres:
|
for formsemestre in formsemestres:
|
||||||
yaml_setup.test_but_jury(formsemestre, doc)
|
yaml_setup_but.but_test_jury(formsemestre, doc)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
@ -102,7 +102,7 @@ def test_but_jury_GEII_lyon(test_client):
|
|||||||
|
|
||||||
# Vérifie les champs de DecisionsProposeesAnnee de tous les semestres:
|
# Vérifie les champs de DecisionsProposeesAnnee de tous les semestres:
|
||||||
for formsemestre in formsemestres:
|
for formsemestre in formsemestres:
|
||||||
yaml_setup.check_deca_fields(formsemestre)
|
yaml_setup_but.check_deca_fields(formsemestre)
|
||||||
|
|
||||||
# Saisie de toutes les décisions de jury "automatiques"
|
# Saisie de toutes les décisions de jury "automatiques"
|
||||||
# et vérification des résultats attendus:
|
# et vérification des résultats attendus:
|
||||||
@ -110,4 +110,4 @@ def test_but_jury_GEII_lyon(test_client):
|
|||||||
formsemestre_validation_auto_but(
|
formsemestre_validation_auto_but(
|
||||||
formsemestre, only_adm=False, no_overwrite=False
|
formsemestre, only_adm=False, no_overwrite=False
|
||||||
)
|
)
|
||||||
yaml_setup.test_but_jury(formsemestre, doc)
|
yaml_setup_but.but_test_jury(formsemestre, doc)
|
||||||
|
@ -13,7 +13,7 @@ Le traitement est effectué dans l'ordre suivant:
|
|||||||
|
|
||||||
setup_from_yaml()
|
setup_from_yaml()
|
||||||
|
|
||||||
- setup_but_formation():
|
- setup_formation():
|
||||||
- import de la formation (le test utilise une seule formation)
|
- import de la formation (le test utilise une seule formation)
|
||||||
- associe_ues_et_parcours():
|
- associe_ues_et_parcours():
|
||||||
- crée les associations formation <-> référentiel de compétence
|
- crée les associations formation <-> référentiel de compétence
|
||||||
@ -43,33 +43,22 @@ et donc marqués par `@pytest.mark.slow`.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import current_app, g
|
from flask import g
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.but.import_refcomp import orebut_import_refcomp
|
|
||||||
from app.but.jury_but import (
|
|
||||||
DecisionsProposeesAnnee,
|
|
||||||
DecisionsProposeesRCUE,
|
|
||||||
DecisionsProposeesUE,
|
|
||||||
)
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcNiveau,
|
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
ApcReferentielCompetences,
|
|
||||||
Evaluation,
|
Evaluation,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
Module,
|
Module,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
ModuleUECoef,
|
|
||||||
UniteEns,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
@ -79,9 +68,10 @@ from app.scodoc import sco_saisie_notes
|
|||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
from tests.conftest import RESOURCES_DIR
|
from tests.conftest import RESOURCES_DIR
|
||||||
|
from tests.unit import yaml_setup_but
|
||||||
|
|
||||||
|
|
||||||
def setup_but_formation(formation_infos: dict) -> Formation:
|
def setup_formation(formation_infos: dict) -> Formation:
|
||||||
"""Importe la formation, qui est lue à partir du fichier XML
|
"""Importe la formation, qui est lue à partir du fichier XML
|
||||||
formation_infos["filename"].
|
formation_infos["filename"].
|
||||||
La formation peut être quelconque, on vérifie juste qu'elle est bien créée.
|
La formation peut être quelconque, on vérifie juste qu'elle est bien créée.
|
||||||
@ -100,94 +90,7 @@ def setup_but_formation(formation_infos: dict) -> Formation:
|
|||||||
return formation
|
return formation
|
||||||
|
|
||||||
|
|
||||||
def setup_formation_referentiel(formation: Formation, refcomp_infos: dict):
|
def create_formsemestre(
|
||||||
"""Associe la formation au référentiel de compétences"""
|
|
||||||
if not refcomp_infos:
|
|
||||||
return
|
|
||||||
refcomp_filename = refcomp_infos["filename"]
|
|
||||||
refcomp_specialite = refcomp_infos["specialite"]
|
|
||||||
# --- Chargement Référentiel
|
|
||||||
if (
|
|
||||||
ApcReferentielCompetences.query.filter_by(
|
|
||||||
scodoc_orig_filename=refcomp_filename, dept_id=g.scodoc_dept_id
|
|
||||||
).first()
|
|
||||||
is None
|
|
||||||
):
|
|
||||||
# pas déjà chargé
|
|
||||||
filename = (
|
|
||||||
Path(current_app.config["SCODOC_DIR"])
|
|
||||||
/ "ressources/referentiels/but2022/competences"
|
|
||||||
/ refcomp_filename
|
|
||||||
)
|
|
||||||
with open(filename, encoding="utf-8") as f:
|
|
||||||
xml_data = f.read()
|
|
||||||
referentiel_competence = orebut_import_refcomp(
|
|
||||||
xml_data, dept_id=g.scodoc_dept_id, orig_filename=Path(filename).name
|
|
||||||
)
|
|
||||||
assert referentiel_competence
|
|
||||||
|
|
||||||
# --- Association au référentiel de compétences
|
|
||||||
referentiel_competence = ApcReferentielCompetences.query.filter_by(
|
|
||||||
specialite=refcomp_specialite
|
|
||||||
).first() # le recherche à nouveau (test)
|
|
||||||
assert referentiel_competence
|
|
||||||
formation.referentiel_competence_id = referentiel_competence.id
|
|
||||||
db.session.add(formation)
|
|
||||||
|
|
||||||
|
|
||||||
def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
|
|
||||||
"""Associe les UE et modules de la formation aux parcours du ref. comp."""
|
|
||||||
referentiel_competence = formation.referentiel_competence
|
|
||||||
if not referentiel_competence:
|
|
||||||
return
|
|
||||||
# --- Association des UEs aux parcours niveaux de compétences
|
|
||||||
for ue_acronyme, ue_infos in formation_infos["ues"].items():
|
|
||||||
ue: UniteEns = formation.ues.filter_by(acronyme=ue_acronyme).first()
|
|
||||||
assert ue is not None # l'UE doit exister dans la formation avec cet acronyme
|
|
||||||
# Parcours:
|
|
||||||
if ue_infos.get("parcours", False):
|
|
||||||
parcour = referentiel_competence.parcours.filter_by(
|
|
||||||
code=ue_infos["parcours"]
|
|
||||||
).first()
|
|
||||||
assert parcour is not None # le parcours indiqué pour cette UE doit exister
|
|
||||||
ue.set_parcour(parcour)
|
|
||||||
|
|
||||||
# Niveaux compétences:
|
|
||||||
competence = referentiel_competence.competences.filter_by(
|
|
||||||
titre=ue_infos["competence"]
|
|
||||||
).first()
|
|
||||||
assert competence is not None # La compétence de titre indiqué doit exister
|
|
||||||
niveau: ApcNiveau = competence.niveaux.filter_by(
|
|
||||||
annee=ue_infos["annee"]
|
|
||||||
).first()
|
|
||||||
assert niveau is not None # le niveau de l'année indiquée doit exister
|
|
||||||
ue.set_niveau_competence(niveau)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
associe_modules_et_parcours(formation, formation_infos)
|
|
||||||
|
|
||||||
|
|
||||||
def associe_modules_et_parcours(formation: Formation, formation_infos: dict):
|
|
||||||
"""Associe les modules à des parcours, grâce au champ modules_parcours"""
|
|
||||||
for code_parcours, codes_modules in formation_infos.get(
|
|
||||||
"modules_parcours", {}
|
|
||||||
).items():
|
|
||||||
parcour = formation.referentiel_competence.parcours.filter_by(
|
|
||||||
code=code_parcours
|
|
||||||
).first()
|
|
||||||
assert parcour is not None # code parcours doit exister dans le ref. comp.
|
|
||||||
for code_module in codes_modules:
|
|
||||||
for module in [
|
|
||||||
module
|
|
||||||
for module in formation.modules
|
|
||||||
if re.match(code_module, module.code)
|
|
||||||
]:
|
|
||||||
module.parcours.append(parcour)
|
|
||||||
db.session.add(module)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def _un_semestre(
|
|
||||||
formation: Formation,
|
formation: Formation,
|
||||||
parcours: list[ApcParcours],
|
parcours: list[ApcParcours],
|
||||||
semestre_id: int,
|
semestre_id: int,
|
||||||
@ -195,7 +98,8 @@ def _un_semestre(
|
|||||||
date_debut: str,
|
date_debut: str,
|
||||||
date_fin: str,
|
date_fin: str,
|
||||||
) -> FormSemestre:
|
) -> FormSemestre:
|
||||||
"Création d'un formsemestre"
|
"Création d'un formsemestre, avec ses modimpls et évaluations"
|
||||||
|
assert formation.is_apc() or not parcours # parcours seulement si APC
|
||||||
formsemestre = FormSemestre(
|
formsemestre = FormSemestre(
|
||||||
formation=formation,
|
formation=formation,
|
||||||
parcours=parcours,
|
parcours=parcours,
|
||||||
@ -230,7 +134,9 @@ def _un_semestre(
|
|||||||
formsemestre.id, default=True, redirect=False
|
formsemestre.id, default=True, redirect=False
|
||||||
)
|
)
|
||||||
_ = sco_groups.create_group(partition_id, default=True)
|
_ = sco_groups.create_group(partition_id, default=True)
|
||||||
|
|
||||||
# Partition de parcours:
|
# Partition de parcours:
|
||||||
|
if formsemestre.formation.is_apc():
|
||||||
formsemestre.setup_parcours_groups()
|
formsemestre.setup_parcours_groups()
|
||||||
|
|
||||||
return formsemestre
|
return formsemestre
|
||||||
@ -248,13 +154,17 @@ def create_evaluations(formsemestre: FormSemestre):
|
|||||||
numero=1,
|
numero=1,
|
||||||
)
|
)
|
||||||
db.session.add(evaluation)
|
db.session.add(evaluation)
|
||||||
|
|
||||||
|
if formsemestre.formation.is_apc():
|
||||||
# Affecte les mêmes poids que les coefs APC:
|
# Affecte les mêmes poids que les coefs APC:
|
||||||
ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef }
|
ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef }
|
||||||
evaluation.set_ue_poids_dict(ue_coef_dict)
|
evaluation.set_ue_poids_dict(ue_coef_dict)
|
||||||
|
|
||||||
|
|
||||||
def note_les_modules(doc: dict):
|
def note_les_modules(doc: dict):
|
||||||
"""Saisie les notes des étudiants"""
|
"""Saisie les notes des étudiants
|
||||||
|
doc : données YAML
|
||||||
|
"""
|
||||||
a_user = User.query.first()
|
a_user = User.query.first()
|
||||||
for nom, infos in doc["Etudiants"].items():
|
for nom, infos in doc["Etudiants"].items():
|
||||||
etud: Identite = Identite.query.filter_by(nom=nom).first()
|
etud: Identite = Identite.query.filter_by(nom=nom).first()
|
||||||
@ -295,16 +205,21 @@ def note_les_modules(doc: dict):
|
|||||||
|
|
||||||
|
|
||||||
def setup_formsemestres(formation: Formation, doc: str):
|
def setup_formsemestres(formation: Formation, doc: str):
|
||||||
"""Création des formsemestres pour tester les parcours BUT"""
|
"""Création des formsemestres
|
||||||
|
Le cas échéant associés à leur(s) parcours.
|
||||||
|
"""
|
||||||
for titre, infos in doc["FormSemestres"].items():
|
for titre, infos in doc["FormSemestres"].items():
|
||||||
|
codes_parcours = infos.get("codes_parcours", [])
|
||||||
|
assert formation.is_apc() or not codes_parcours # parcours seulement en APC
|
||||||
parcours = []
|
parcours = []
|
||||||
for code_parcour in infos.get("codes_parcours", []):
|
for code_parcour in codes_parcours:
|
||||||
parcour = formation.referentiel_competence.parcours.filter_by(
|
parcour = formation.referentiel_competence.parcours.filter_by(
|
||||||
code=code_parcour
|
code=code_parcour
|
||||||
).first()
|
).first()
|
||||||
assert parcour is not None
|
assert parcour is not None
|
||||||
parcours.append(parcour)
|
parcours.append(parcour)
|
||||||
_ = _un_semestre(
|
|
||||||
|
_ = create_formsemestre(
|
||||||
formation,
|
formation,
|
||||||
parcours,
|
parcours,
|
||||||
infos["idx"],
|
infos["idx"],
|
||||||
@ -362,193 +277,12 @@ def setup_from_yaml(filename: str) -> dict:
|
|||||||
with open(filename, encoding="utf-8") as f:
|
with open(filename, encoding="utf-8") as f:
|
||||||
doc = yaml.safe_load(f.read())
|
doc = yaml.safe_load(f.read())
|
||||||
|
|
||||||
formation = setup_but_formation(doc["Formation"])
|
formation = setup_formation(doc["Formation"])
|
||||||
setup_formation_referentiel(formation, doc.get("ReferentielCompetences", {}))
|
yaml_setup_but.setup_formation_referentiel(
|
||||||
associe_ues_et_parcours(formation, doc["Formation"])
|
formation, doc.get("ReferentielCompetences", {})
|
||||||
|
)
|
||||||
|
yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])
|
||||||
setup_formsemestres(formation, doc)
|
setup_formsemestres(formation, doc)
|
||||||
inscrit_les_etudiants(formation, doc)
|
inscrit_les_etudiants(formation, doc)
|
||||||
note_les_modules(doc)
|
note_les_modules(doc)
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
def _check_codes_jury(codes: list[str], codes_att: list[str]):
|
|
||||||
"""Vérifie (assert) la liste des codes
|
|
||||||
l'ordre n'a pas d'importance ici.
|
|
||||||
Si codes_att contient un "...", on se contente de vérifier que
|
|
||||||
les codes de codes_att sont tous présents dans codes.
|
|
||||||
"""
|
|
||||||
codes_set = set(codes)
|
|
||||||
codes_att_set = set(codes_att)
|
|
||||||
if "..." in codes_att_set:
|
|
||||||
codes_att_set.remove("...")
|
|
||||||
assert codes_att_set.issubset(codes_set)
|
|
||||||
else:
|
|
||||||
assert codes_att_set == codes_set
|
|
||||||
|
|
||||||
|
|
||||||
def _check_decisions_ues(
|
|
||||||
decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict]
|
|
||||||
):
|
|
||||||
"""Vérifie les décisions d'UE
|
|
||||||
puis enregistre décision manuelle si indiquée dans le YAML.
|
|
||||||
"""
|
|
||||||
for acronyme, dec_ue_att in decisions_ues_att.items():
|
|
||||||
# retrouve l'UE
|
|
||||||
ues_d = [
|
|
||||||
dec_ue
|
|
||||||
for dec_ue in decisions_ues.values()
|
|
||||||
if dec_ue.ue.acronyme == acronyme
|
|
||||||
]
|
|
||||||
assert len(ues_d) == 1 # une et une seule UE avec l'acronyme indiqué
|
|
||||||
dec_ue = ues_d[0]
|
|
||||||
if "codes" in dec_ue_att:
|
|
||||||
_check_codes_jury(dec_ue.codes, dec_ue_att["codes"])
|
|
||||||
|
|
||||||
for attr in ("explanation", "code_valide"):
|
|
||||||
if attr in dec_ue_att:
|
|
||||||
if getattr(dec_ue, attr) != dec_ue_att[attr]:
|
|
||||||
raise ValueError(
|
|
||||||
f"""Erreur: décision d'UE: {dec_ue.ue.acronyme
|
|
||||||
} : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}"""
|
|
||||||
)
|
|
||||||
for attr in ("moy_ue", "moy_ue_with_cap"):
|
|
||||||
if attr in dec_ue_att:
|
|
||||||
assert (
|
|
||||||
abs(getattr(dec_ue, attr) - dec_ue_att[attr]) < scu.NOTES_PRECISION
|
|
||||||
)
|
|
||||||
# Force décision de jury:
|
|
||||||
code_manuel = dec_ue_att.get("decision_jury")
|
|
||||||
if code_manuel is not None:
|
|
||||||
assert code_manuel in dec_ue.codes
|
|
||||||
dec_ue.record(code_manuel)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_decisions_rcues(
|
|
||||||
decisions_rcues: list[DecisionsProposeesRCUE], decisions_rcues_att: dict
|
|
||||||
):
|
|
||||||
"Vérifie les décisions d'RCUEs"
|
|
||||||
for acronyme, dec_rcue_att in decisions_rcues_att.items():
|
|
||||||
# retrouve la décision RCUE à partir de l'acronyme de la 1ère UE
|
|
||||||
rcues_d = [
|
|
||||||
dec_rcue
|
|
||||||
for dec_rcue in decisions_rcues
|
|
||||||
if dec_rcue.rcue.ue_1.acronyme == acronyme
|
|
||||||
]
|
|
||||||
assert len(rcues_d) == 1 # un et un seul RCUE avec l'UE d'acronyme indiqué
|
|
||||||
dec_rcue = rcues_d[0]
|
|
||||||
if "codes" in dec_rcue_att:
|
|
||||||
_check_codes_jury(dec_rcue.codes, dec_rcue_att["codes"])
|
|
||||||
for attr in ("explanation", "code_valide"):
|
|
||||||
if attr in dec_rcue_att:
|
|
||||||
assert getattr(dec_rcue, attr) == dec_rcue_att[attr]
|
|
||||||
# Descend dans le RCUE:
|
|
||||||
if "rcue" in dec_rcue_att:
|
|
||||||
if "moy_rcue" in dec_rcue_att["rcue"]:
|
|
||||||
assert (
|
|
||||||
abs(dec_rcue.rcue.moy_rcue - dec_rcue_att["rcue"]["moy_rcue"])
|
|
||||||
< scu.NOTES_PRECISION
|
|
||||||
)
|
|
||||||
if "est_compensable" in dec_rcue_att["rcue"]:
|
|
||||||
assert (
|
|
||||||
dec_rcue.rcue.est_compensable()
|
|
||||||
== dec_rcue_att["rcue"]["est_compensable"]
|
|
||||||
)
|
|
||||||
# Force décision de jury:
|
|
||||||
code_manuel = dec_rcue_att.get("decision_jury")
|
|
||||||
if code_manuel is not None:
|
|
||||||
assert code_manuel in dec_rcue.codes
|
|
||||||
dec_rcue.record(code_manuel)
|
|
||||||
|
|
||||||
|
|
||||||
def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict):
|
|
||||||
"""Vérifie que les résultats de jury calculés pour l'année, les RCUEs et les UEs
|
|
||||||
sont ceux attendus,
|
|
||||||
puis enregistre les décisions manuelles indiquées dans le YAML.
|
|
||||||
|
|
||||||
deca est le résultat calculé par ScoDoc
|
|
||||||
deca_att est un dict lu du YAML
|
|
||||||
"""
|
|
||||||
if "codes" in deca_att:
|
|
||||||
_check_codes_jury(deca.codes, deca_att["codes"])
|
|
||||||
|
|
||||||
for attr in ("passage_de_droit", "code_valide", "nb_competences"):
|
|
||||||
if attr in deca_att:
|
|
||||||
assert getattr(deca, attr) == deca_att[attr]
|
|
||||||
|
|
||||||
if "decisions_ues" in deca_att:
|
|
||||||
_check_decisions_ues(deca.decisions_ues, deca_att["decisions_ues"])
|
|
||||||
|
|
||||||
if "nb_rcues_annee" in deca_att:
|
|
||||||
assert deca_att["nb_rcues_annee"] == len(deca.rcues_annee)
|
|
||||||
|
|
||||||
if "decisions_rcues" in deca_att:
|
|
||||||
_check_decisions_rcues(
|
|
||||||
deca.decisions_rcue_by_niveau.values(), deca_att["decisions_rcues"]
|
|
||||||
)
|
|
||||||
# Force décision de jury:
|
|
||||||
code_manuel = deca_att.get("decision_jury")
|
|
||||||
if code_manuel is not None:
|
|
||||||
assert code_manuel in deca.codes
|
|
||||||
deca.record(code_manuel)
|
|
||||||
assert deca.recorded
|
|
||||||
|
|
||||||
|
|
||||||
def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None):
|
|
||||||
"""Vérifie les champs principaux (inscription, nb UE, nb compétences)
|
|
||||||
de l'instance de DecisionsProposeesAnnee.
|
|
||||||
Ne vérifie pas les décisions de jury proprement dites.
|
|
||||||
Si etud n'est pas spécifié, prend le premier inscrit trouvé dans le semestre.
|
|
||||||
"""
|
|
||||||
etud = etud or formsemestre.etuds.first()
|
|
||||||
assert etud # il faut au moins un étudiant dans le semestre
|
|
||||||
deca = DecisionsProposeesAnnee(etud, formsemestre)
|
|
||||||
assert deca.validation is None # pas encore de validation enregistrée
|
|
||||||
assert False is deca.recorded
|
|
||||||
assert deca.code_valide is None
|
|
||||||
if formsemestre.semestre_id % 2:
|
|
||||||
assert deca.formsemestre_impair == formsemestre
|
|
||||||
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair
|
|
||||||
else:
|
|
||||||
assert deca.formsemestre_pair == formsemestre
|
|
||||||
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair
|
|
||||||
assert deca.inscription_etat == scu.INSCRIT
|
|
||||||
assert deca.inscription_etat_impair == scu.INSCRIT
|
|
||||||
assert (deca.parcour is None) or (
|
|
||||||
deca.parcour.id in {p.id for p in formsemestre.parcours}
|
|
||||||
)
|
|
||||||
|
|
||||||
nb_ues = (
|
|
||||||
len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
|
|
||||||
if deca.formsemestre_pair
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
nb_ues += (
|
|
||||||
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
|
|
||||||
if deca.formsemestre_impair
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
assert len(deca.decisions_ues) == nb_ues
|
|
||||||
|
|
||||||
nb_ues_un_sem = (
|
|
||||||
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
|
|
||||||
if deca.formsemestre_impair
|
|
||||||
else len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
|
|
||||||
)
|
|
||||||
assert len(deca.niveaux_competences) == nb_ues_un_sem
|
|
||||||
assert deca.nb_competences == nb_ues_un_sem
|
|
||||||
|
|
||||||
|
|
||||||
def test_but_jury(formsemestre: FormSemestre, doc: dict):
|
|
||||||
"""Test jurys BUT
|
|
||||||
Vérifie les champs de DecisionsProposeesAnnee et UEs
|
|
||||||
"""
|
|
||||||
for etud in formsemestre.etuds:
|
|
||||||
deca = DecisionsProposeesAnnee(etud, formsemestre)
|
|
||||||
doc_formsemestre = doc["Etudiants"][etud.nom]["formsemestres"][
|
|
||||||
formsemestre.titre
|
|
||||||
]
|
|
||||||
assert doc_formsemestre
|
|
||||||
if "attendu" in doc_formsemestre:
|
|
||||||
if "deca" in doc_formsemestre["attendu"]:
|
|
||||||
deca_att = doc_formsemestre["attendu"]["deca"]
|
|
||||||
compare_decisions_annee(deca, deca_att)
|
|
||||||
|
304
tests/unit/yaml_setup_but.py
Normal file
304
tests/unit/yaml_setup_but.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Mise en place pour tests unitaires à partir de descriptions YAML:
|
||||||
|
fonctions spécifiques au BUT
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from flask import current_app, g
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.but.import_refcomp import orebut_import_refcomp
|
||||||
|
from app.but.jury_but import (
|
||||||
|
DecisionsProposeesAnnee,
|
||||||
|
DecisionsProposeesRCUE,
|
||||||
|
DecisionsProposeesUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.models import (
|
||||||
|
ApcNiveau,
|
||||||
|
ApcReferentielCompetences,
|
||||||
|
Formation,
|
||||||
|
FormSemestre,
|
||||||
|
Identite,
|
||||||
|
UniteEns,
|
||||||
|
)
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
def setup_formation_referentiel(formation: Formation, refcomp_infos: dict):
|
||||||
|
"""Si il y a un référentiel de compétences, indiqué dans le YAML,
|
||||||
|
le charge au besoin et l'associe à la formation.
|
||||||
|
"""
|
||||||
|
if not refcomp_infos:
|
||||||
|
return
|
||||||
|
assert formation.is_apc() # si ref; comp., doit être APC
|
||||||
|
refcomp_filename = refcomp_infos["filename"]
|
||||||
|
refcomp_specialite = refcomp_infos["specialite"]
|
||||||
|
# --- Chargement Référentiel
|
||||||
|
if (
|
||||||
|
ApcReferentielCompetences.query.filter_by(
|
||||||
|
scodoc_orig_filename=refcomp_filename, dept_id=g.scodoc_dept_id
|
||||||
|
).first()
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
# pas déjà chargé
|
||||||
|
filename = (
|
||||||
|
Path(current_app.config["SCODOC_DIR"])
|
||||||
|
/ "ressources/referentiels/but2022/competences"
|
||||||
|
/ refcomp_filename
|
||||||
|
)
|
||||||
|
with open(filename, encoding="utf-8") as f:
|
||||||
|
xml_data = f.read()
|
||||||
|
referentiel_competence = orebut_import_refcomp(
|
||||||
|
xml_data, dept_id=g.scodoc_dept_id, orig_filename=Path(filename).name
|
||||||
|
)
|
||||||
|
assert referentiel_competence
|
||||||
|
|
||||||
|
# --- Association au référentiel de compétences
|
||||||
|
referentiel_competence = ApcReferentielCompetences.query.filter_by(
|
||||||
|
specialite=refcomp_specialite
|
||||||
|
).first() # le recherche à nouveau (test)
|
||||||
|
assert referentiel_competence
|
||||||
|
formation.referentiel_competence_id = referentiel_competence.id
|
||||||
|
db.session.add(formation)
|
||||||
|
|
||||||
|
|
||||||
|
def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
|
||||||
|
"""Associe les UE et modules de la formation aux parcours du ref. comp."""
|
||||||
|
referentiel_competence = formation.referentiel_competence
|
||||||
|
if not referentiel_competence:
|
||||||
|
return
|
||||||
|
# --- Association des UEs aux parcours niveaux de compétences
|
||||||
|
for ue_acronyme, ue_infos in formation_infos["ues"].items():
|
||||||
|
ue: UniteEns = formation.ues.filter_by(acronyme=ue_acronyme).first()
|
||||||
|
assert ue is not None # l'UE doit exister dans la formation avec cet acronyme
|
||||||
|
# Parcours:
|
||||||
|
if ue_infos.get("parcours", False):
|
||||||
|
parcour = referentiel_competence.parcours.filter_by(
|
||||||
|
code=ue_infos["parcours"]
|
||||||
|
).first()
|
||||||
|
assert parcour is not None # le parcours indiqué pour cette UE doit exister
|
||||||
|
ue.set_parcour(parcour)
|
||||||
|
|
||||||
|
# Niveaux compétences:
|
||||||
|
competence = referentiel_competence.competences.filter_by(
|
||||||
|
titre=ue_infos["competence"]
|
||||||
|
).first()
|
||||||
|
assert competence is not None # La compétence de titre indiqué doit exister
|
||||||
|
niveau: ApcNiveau = competence.niveaux.filter_by(
|
||||||
|
annee=ue_infos["annee"]
|
||||||
|
).first()
|
||||||
|
assert niveau is not None # le niveau de l'année indiquée doit exister
|
||||||
|
ue.set_niveau_competence(niveau)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
associe_modules_et_parcours(formation, formation_infos)
|
||||||
|
|
||||||
|
|
||||||
|
def associe_modules_et_parcours(formation: Formation, formation_infos: dict):
|
||||||
|
"""Associe les modules à des parcours, grâce au champ modules_parcours"""
|
||||||
|
for code_parcours, codes_modules in formation_infos.get(
|
||||||
|
"modules_parcours", {}
|
||||||
|
).items():
|
||||||
|
parcour = formation.referentiel_competence.parcours.filter_by(
|
||||||
|
code=code_parcours
|
||||||
|
).first()
|
||||||
|
assert parcour is not None # code parcours doit exister dans le ref. comp.
|
||||||
|
for code_module in codes_modules:
|
||||||
|
for module in [
|
||||||
|
module
|
||||||
|
for module in formation.modules
|
||||||
|
if re.match(code_module, module.code)
|
||||||
|
]:
|
||||||
|
module.parcours.append(parcour)
|
||||||
|
db.session.add(module)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_codes_jury(codes: list[str], codes_att: list[str]):
|
||||||
|
"""Vérifie (assert) la liste des codes
|
||||||
|
l'ordre n'a pas d'importance ici.
|
||||||
|
Si codes_att contient un "...", on se contente de vérifier que
|
||||||
|
les codes de codes_att sont tous présents dans codes.
|
||||||
|
"""
|
||||||
|
codes_set = set(codes)
|
||||||
|
codes_att_set = set(codes_att)
|
||||||
|
if "..." in codes_att_set:
|
||||||
|
codes_att_set.remove("...")
|
||||||
|
assert codes_att_set.issubset(codes_set)
|
||||||
|
else:
|
||||||
|
assert codes_att_set == codes_set
|
||||||
|
|
||||||
|
|
||||||
|
def but_check_decisions_ues(
|
||||||
|
decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict]
|
||||||
|
):
|
||||||
|
"""Vérifie les décisions d'UE
|
||||||
|
puis enregistre décision manuelle si indiquée dans le YAML.
|
||||||
|
"""
|
||||||
|
for acronyme, dec_ue_att in decisions_ues_att.items():
|
||||||
|
# retrouve l'UE
|
||||||
|
ues_d = [
|
||||||
|
dec_ue
|
||||||
|
for dec_ue in decisions_ues.values()
|
||||||
|
if dec_ue.ue.acronyme == acronyme
|
||||||
|
]
|
||||||
|
assert len(ues_d) == 1 # une et une seule UE avec l'acronyme indiqué
|
||||||
|
dec_ue = ues_d[0]
|
||||||
|
if "codes" in dec_ue_att:
|
||||||
|
_check_codes_jury(dec_ue.codes, dec_ue_att["codes"])
|
||||||
|
|
||||||
|
for attr in ("explanation", "code_valide"):
|
||||||
|
if attr in dec_ue_att:
|
||||||
|
if getattr(dec_ue, attr) != dec_ue_att[attr]:
|
||||||
|
raise ValueError(
|
||||||
|
f"""Erreur: décision d'UE: {dec_ue.ue.acronyme
|
||||||
|
} : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}"""
|
||||||
|
)
|
||||||
|
for attr in ("moy_ue", "moy_ue_with_cap"):
|
||||||
|
if attr in dec_ue_att:
|
||||||
|
assert (
|
||||||
|
abs(getattr(dec_ue, attr) - dec_ue_att[attr]) < scu.NOTES_PRECISION
|
||||||
|
)
|
||||||
|
# Force décision de jury:
|
||||||
|
code_manuel = dec_ue_att.get("decision_jury")
|
||||||
|
if code_manuel is not None:
|
||||||
|
assert code_manuel in dec_ue.codes
|
||||||
|
dec_ue.record(code_manuel)
|
||||||
|
|
||||||
|
|
||||||
|
def but_check_decisions_rcues(
|
||||||
|
decisions_rcues: list[DecisionsProposeesRCUE], decisions_rcues_att: dict
|
||||||
|
):
|
||||||
|
"Vérifie les décisions d'RCUEs"
|
||||||
|
for acronyme, dec_rcue_att in decisions_rcues_att.items():
|
||||||
|
# retrouve la décision RCUE à partir de l'acronyme de la 1ère UE
|
||||||
|
rcues_d = [
|
||||||
|
dec_rcue
|
||||||
|
for dec_rcue in decisions_rcues
|
||||||
|
if dec_rcue.rcue.ue_1.acronyme == acronyme
|
||||||
|
]
|
||||||
|
assert len(rcues_d) == 1 # un et un seul RCUE avec l'UE d'acronyme indiqué
|
||||||
|
dec_rcue = rcues_d[0]
|
||||||
|
if "codes" in dec_rcue_att:
|
||||||
|
_check_codes_jury(dec_rcue.codes, dec_rcue_att["codes"])
|
||||||
|
for attr in ("explanation", "code_valide"):
|
||||||
|
if attr in dec_rcue_att:
|
||||||
|
assert getattr(dec_rcue, attr) == dec_rcue_att[attr]
|
||||||
|
# Descend dans le RCUE:
|
||||||
|
if "rcue" in dec_rcue_att:
|
||||||
|
if "moy_rcue" in dec_rcue_att["rcue"]:
|
||||||
|
assert (
|
||||||
|
abs(dec_rcue.rcue.moy_rcue - dec_rcue_att["rcue"]["moy_rcue"])
|
||||||
|
< scu.NOTES_PRECISION
|
||||||
|
)
|
||||||
|
if "est_compensable" in dec_rcue_att["rcue"]:
|
||||||
|
assert (
|
||||||
|
dec_rcue.rcue.est_compensable()
|
||||||
|
== dec_rcue_att["rcue"]["est_compensable"]
|
||||||
|
)
|
||||||
|
# Force décision de jury:
|
||||||
|
code_manuel = dec_rcue_att.get("decision_jury")
|
||||||
|
if code_manuel is not None:
|
||||||
|
assert code_manuel in dec_rcue.codes
|
||||||
|
dec_rcue.record(code_manuel)
|
||||||
|
|
||||||
|
|
||||||
|
def but_compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict):
|
||||||
|
"""Vérifie que les résultats de jury calculés pour l'année, les RCUEs et les UEs
|
||||||
|
sont ceux attendus,
|
||||||
|
puis enregistre les décisions manuelles indiquées dans le YAML.
|
||||||
|
|
||||||
|
deca est le résultat calculé par ScoDoc
|
||||||
|
deca_att est un dict lu du YAML
|
||||||
|
"""
|
||||||
|
if "codes" in deca_att:
|
||||||
|
_check_codes_jury(deca.codes, deca_att["codes"])
|
||||||
|
|
||||||
|
for attr in ("passage_de_droit", "code_valide", "nb_competences"):
|
||||||
|
if attr in deca_att:
|
||||||
|
assert getattr(deca, attr) == deca_att[attr]
|
||||||
|
|
||||||
|
if "decisions_ues" in deca_att:
|
||||||
|
but_check_decisions_ues(deca.decisions_ues, deca_att["decisions_ues"])
|
||||||
|
|
||||||
|
if "nb_rcues_annee" in deca_att:
|
||||||
|
assert deca_att["nb_rcues_annee"] == len(deca.rcues_annee)
|
||||||
|
|
||||||
|
if "decisions_rcues" in deca_att:
|
||||||
|
but_check_decisions_rcues(
|
||||||
|
deca.decisions_rcue_by_niveau.values(), deca_att["decisions_rcues"]
|
||||||
|
)
|
||||||
|
# Force décision de jury:
|
||||||
|
code_manuel = deca_att.get("decision_jury")
|
||||||
|
if code_manuel is not None:
|
||||||
|
assert code_manuel in deca.codes
|
||||||
|
deca.record(code_manuel)
|
||||||
|
assert deca.recorded
|
||||||
|
|
||||||
|
|
||||||
|
def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None):
|
||||||
|
"""Vérifie les champs principaux (inscription, nb UE, nb compétences)
|
||||||
|
de l'instance de DecisionsProposeesAnnee.
|
||||||
|
Ne vérifie pas les décisions de jury proprement dites.
|
||||||
|
Si etud n'est pas spécifié, prend le premier inscrit trouvé dans le semestre.
|
||||||
|
"""
|
||||||
|
etud = etud or formsemestre.etuds.first()
|
||||||
|
assert etud # il faut au moins un étudiant dans le semestre
|
||||||
|
deca = DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
assert deca.validation is None # pas encore de validation enregistrée
|
||||||
|
assert False is deca.recorded
|
||||||
|
assert deca.code_valide is None
|
||||||
|
if formsemestre.semestre_id % 2:
|
||||||
|
assert deca.formsemestre_impair == formsemestre
|
||||||
|
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair
|
||||||
|
else:
|
||||||
|
assert deca.formsemestre_pair == formsemestre
|
||||||
|
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair
|
||||||
|
assert deca.inscription_etat == scu.INSCRIT
|
||||||
|
assert deca.inscription_etat_impair == scu.INSCRIT
|
||||||
|
assert (deca.parcour is None) or (
|
||||||
|
deca.parcour.id in {p.id for p in formsemestre.parcours}
|
||||||
|
)
|
||||||
|
|
||||||
|
nb_ues = (
|
||||||
|
len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
|
||||||
|
if deca.formsemestre_pair
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
nb_ues += (
|
||||||
|
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
|
||||||
|
if deca.formsemestre_impair
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
assert len(deca.decisions_ues) == nb_ues
|
||||||
|
|
||||||
|
nb_ues_un_sem = (
|
||||||
|
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
|
||||||
|
if deca.formsemestre_impair
|
||||||
|
else len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
|
||||||
|
)
|
||||||
|
assert len(deca.niveaux_competences) == nb_ues_un_sem
|
||||||
|
assert deca.nb_competences == nb_ues_un_sem
|
||||||
|
|
||||||
|
|
||||||
|
def but_test_jury(formsemestre: FormSemestre, doc: dict):
|
||||||
|
"""Test jurys BUT
|
||||||
|
Vérifie les champs de DecisionsProposeesAnnee et UEs
|
||||||
|
"""
|
||||||
|
for etud in formsemestre.etuds:
|
||||||
|
deca = DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
doc_formsemestre = doc["Etudiants"][etud.nom]["formsemestres"][
|
||||||
|
formsemestre.titre
|
||||||
|
]
|
||||||
|
assert doc_formsemestre
|
||||||
|
if "attendu" in doc_formsemestre:
|
||||||
|
if "deca" in doc_formsemestre["attendu"]:
|
||||||
|
deca_att = doc_formsemestre["attendu"]["deca"]
|
||||||
|
but_compare_decisions_annee(deca, deca_att)
|
Loading…
Reference in New Issue
Block a user