Tests YAML: séparation fct spécifiques BUT

This commit is contained in:
Emmanuel Viennet 2023-02-11 05:04:10 +01:00
parent b089773766
commit e11b7fa076
4 changed files with 346 additions and 308 deletions

View File

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

View File

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

View File

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

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