Tests YAML jury BUT: amélioration code test + yaml GEII Lyon ok

This commit is contained in:
Emmanuel Viennet 2023-01-09 18:46:27 -03:00 committed by iziram
parent 00c09b1eb8
commit dd93d952d7
5 changed files with 150 additions and 86 deletions

View File

@ -170,7 +170,7 @@ class DecisionsProposees:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} valid={self.code_valide return f"""<{self.__class__.__name__} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}""" } codes={self.codes} explanation={self.explanation}>"""
class DecisionsProposeesAnnee(DecisionsProposees): class DecisionsProposeesAnnee(DecisionsProposees):
@ -732,7 +732,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None: if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def record_all(self): def record_all(self, no_overwrite: bool = True):
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire, """Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique" et sont donc en mode "automatique"
""" """
@ -746,7 +746,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# rappel: le code par défaut est en tête # rappel: le code par défaut est en tête
code = dec.codes[0] if dec.codes else None code = dec.codes[0] if dec.codes else None
# enregistre le code jury seulement s'il n'y a pas déjà de code # enregistre le code jury seulement s'il n'y a pas déjà de code
dec.record(code, no_overwrite=True) # (no_overwrite=True) sauf en mode test yaml
dec.record(code, no_overwrite=no_overwrite)
def erase(self, only_one_sem=False): def erase(self, only_one_sem=False):
"""Efface les décisions de jury de cet étudiant """Efface les décisions de jury de cet étudiant
@ -922,6 +923,10 @@ class DecisionsProposeesRCUE(DecisionsProposees):
else: else:
self.codes.insert(1, self.code_valide) self.codes.insert(1, self.code_valide)
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}"""
def record(self, code: str, no_overwrite=False): def record(self, code: str, no_overwrite=False):
"""Enregistre le code""" """Enregistre le code"""
if self.rcue is None: if self.rcue is None:
@ -1073,7 +1078,7 @@ class DecisionsProposeesUE(DecisionsProposees):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}""" } codes={self.codes} explanation={self.explanation}>"""
def set_rcue(self, rcue: RegroupementCoherentUE): def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes """Rattache cette UE à un RCUE. Cela peut modifier les codes

View File

@ -15,12 +15,17 @@ from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(formsemestre: FormSemestre, only_adm=True) -> int: def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
) -> int:
"""Calcul automatique des décisions de jury sur une année BUT. """Calcul automatique des décisions de jury sur une année BUT.
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit).
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc Si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests) (mode à n'utiliser que pour les tests)
Si no_overwrite est vrai (défaut), ne -écrit jamais les codes déjà enregistrés
(utiliser faux pour certains tests)
Returns: nombre d'étudiants "admis" Returns: nombre d'étudiants "admis"
""" """
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
@ -33,7 +38,7 @@ def formsemestre_validation_auto_but(formsemestre: FormSemestre, only_adm=True)
if deca.admis: # année réussie if deca.admis: # année réussie
nb_admis += 1 nb_admis += 1
if deca.admis or not only_adm: if deca.admis or not only_adm:
deca.record_all() deca.record_all(no_overwrite=no_overwrite)
db.session.commit() db.session.commit()
return nb_admis return nb_admis

View File

@ -194,12 +194,11 @@ Etudiants:
decision_jury: CMP decision_jury: CMP
rcue: rcue:
moy_rcue: 10.75 moy_rcue: 10.75
est_compensable: False est_compensable: True
"UE12": "UE12":
code_valide: CMP code_valide: CMP # car validé en fin de S2
decision_jury: CMP
rcue: rcue:
moy_rcue: 10.50 moy_rcue: 9.50 # la moyenne courante (et non enregistrée), donc pas 10.5
est_compensable: False est_compensable: False
decision_annee: ADM decision_annee: ADM

View File

@ -35,7 +35,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:
_check_deca(formsemestre) yaml_setup.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):
@ -43,11 +43,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()
_test_but_jury(S1, doc) yaml_setup.test_but_jury(S1, doc)
S2: FormSemestre = FormSemestre.query.filter_by(titre="S2_SEE").first() S2: FormSemestre = FormSemestre.query.filter_by(titre="S2_SEE").first()
_test_but_jury(S2, doc) yaml_setup.test_but_jury(S2, doc)
S3: FormSemestre = FormSemestre.query.filter_by(titre="S3").first() S3: FormSemestre = FormSemestre.query.filter_by(titre="S3").first()
_test_but_jury(S3, doc) yaml_setup.test_but_jury(S3, doc)
# _test_but_jury(S1_redoublant, doc) # _test_but_jury(S1_redoublant, doc)
@ -66,7 +66,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:
_check_deca(formsemestre) yaml_setup.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:
@ -74,7 +74,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:
_test_but_jury(formsemestre, doc) yaml_setup.test_but_jury(formsemestre, doc)
@pytest.mark.slow @pytest.mark.slow
@ -90,71 +90,14 @@ def test_but_jury_GEII_lyon(test_client):
FormSemestre.date_debut, FormSemestre.semestre_id FormSemestre.date_debut, FormSemestre.semestre_id
).all() ).all()
# Vérifie les deca de tous les semestres: # Vérifie les champs de DecisionsProposeesAnnee de tous les semestres:
for formsemestre in formsemestres: for formsemestre in formsemestres:
_check_deca(formsemestre) yaml_setup.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 "automatiques"
# et vérification des résultats attendus:
for formsemestre in formsemestres: for formsemestre in formsemestres:
formsemestre_validation_auto_but(formsemestre, only_adm=False) formsemestre_validation_auto_but(
formsemestre, only_adm=False, no_overwrite=False
# Vérifie résultats attendus: )
for formsemestre in formsemestres: yaml_setup.test_but_jury(formsemestre, doc)
_test_but_jury(formsemestre, doc)
def _check_deca(formsemestre: FormSemestre, etud: Identite = None):
"""vérifie les champs principaux de l'instance de DecisionsProposeesAnnee"""
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
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"]
yaml_setup.compare_decisions_annee(deca, deca_att)

View File

@ -1,7 +1,45 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
""" """
Met en place une base pour les tests, à partir d'une description YAML Met en place une base pour les tests unitaires, à partir d'une description
qui peut donner la formation, son ref. compétences, les formsemestres, YAML qui peut donner la formation, son ref. compétences, les formsemestres,
les étudiants et leurs notes. les étudiants et leurs notes et décisions de jury.
Le traitement est effectué dans l'ordre suivant:
setup_from_yaml()
- setup_but_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_formsemestres()
- 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 après 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 os
@ -352,7 +390,7 @@ def _check_decisions_ues(
decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict] decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict]
): ):
"""Vérifie les décisions d'UE """Vérifie les décisions d'UE
pui enregistre décision manuelle si indiquée. puis enregistre décision manuelle si indiquée dans le YAML.
""" """
for acronyme, dec_ue_att in decisions_ues_att.items(): for acronyme, dec_ue_att in decisions_ues_att.items():
# retrouve l'UE # retrouve l'UE
@ -410,11 +448,18 @@ def _check_decisions_rcues(
dec_rcue.rcue.est_compensable() dec_rcue.rcue.est_compensable()
== dec_rcue_att["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): def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict):
"""Vérifie que les résultats de jury calculés sont ceux attendus, """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. puis enregistre les décisions manuelles indiquées dans le YAML.
deca est le résultat calculé par ScoDoc deca est le résultat calculé par ScoDoc
deca_att est un dict lu du YAML deca_att est un dict lu du YAML
""" """
@ -435,3 +480,70 @@ def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict):
_check_decisions_rcues( _check_decisions_rcues(
deca.decisions_rcue_by_niveau.values(), deca_att["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)