diff --git a/app/but/jury_but.py b/app/but/jury_but.py index e01703ad..d2d1039b 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -170,7 +170,7 @@ class DecisionsProposees: def __repr__(self) -> str: return f"""<{self.__class__.__name__} valid={self.code_valide - } codes={self.codes} explanation={self.explanation}""" + } codes={self.codes} explanation={self.explanation}>""" class DecisionsProposeesAnnee(DecisionsProposees): @@ -732,7 +732,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): if self.formsemestre_pair is not None: 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, et sont donc en mode "automatique" """ @@ -746,7 +746,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): # rappel: le code par défaut est en tête code = dec.codes[0] if dec.codes else None # 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): """Efface les décisions de jury de cet étudiant @@ -922,6 +923,10 @@ class DecisionsProposeesRCUE(DecisionsProposees): else: 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): """Enregistre le code""" if self.rcue is None: @@ -1073,7 +1078,7 @@ class DecisionsProposeesUE(DecisionsProposees): def __repr__(self) -> str: 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): """Rattache cette UE à un RCUE. Cela peut modifier les codes diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py index 6bcbfa61..a3101b83 100644 --- a/app/but/jury_but_validation_auto.py +++ b/app/but/jury_but_validation_auto.py @@ -15,12 +15,17 @@ from app.scodoc import sco_cache 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. 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 (mode à n'utiliser que pour les tests) + Si no_overwrite est vrai (défaut), ne ré-écrit jamais les codes déjà enregistrés + (utiliser faux pour certains tests) + Returns: nombre d'étudiants "admis" """ 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 nb_admis += 1 if deca.admis or not only_adm: - deca.record_all() + deca.record_all(no_overwrite=no_overwrite) db.session.commit() return nb_admis diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml index 9b4b21af..e73b7b1d 100644 --- a/tests/unit/cursus_but_geii_lyon.yaml +++ b/tests/unit/cursus_but_geii_lyon.yaml @@ -194,12 +194,11 @@ Etudiants: decision_jury: CMP rcue: moy_rcue: 10.75 - est_compensable: False + est_compensable: True "UE12": - code_valide: CMP - decision_jury: CMP + code_valide: CMP # car validé en fin de S2 rcue: - moy_rcue: 10.50 + moy_rcue: 9.50 # la moyenne courante (et non enregistrée), donc pas 10.5 est_compensable: False decision_annee: ADM \ No newline at end of file diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index f4b51b6b..fa75ae32 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -35,7 +35,7 @@ def test_but_jury_GB(test_client): # Vérifie les deca de tous les semestres: for formsemestre in FormSemestre.query: - _check_deca(formsemestre) + yaml_setup.check_deca_fields(formsemestre) # Saisie de toutes les décisions de jury 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: 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() - _test_but_jury(S2, doc) + yaml_setup.test_but_jury(S2, doc) 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) @@ -66,7 +66,7 @@ def test_but_jury_GMP_lm(test_client): # Vérifie les deca de tous les semestres: 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à for formsemestre in formsemestres: @@ -74,7 +74,7 @@ def test_but_jury_GMP_lm(test_client): # Vérifie résultats attendus: for formsemestre in formsemestres: - _test_but_jury(formsemestre, doc) + yaml_setup.test_but_jury(formsemestre, doc) @pytest.mark.slow @@ -90,71 +90,14 @@ def test_but_jury_GEII_lyon(test_client): FormSemestre.date_debut, FormSemestre.semestre_id ).all() - # Vérifie les deca de tous les semestres: + # Vérifie les champs de DecisionsProposeesAnnee de tous les semestres: 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: - formsemestre_validation_auto_but(formsemestre, only_adm=False) - - # Vérifie résultats attendus: - for formsemestre in formsemestres: - _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) + formsemestre_validation_auto_but( + formsemestre, only_adm=False, no_overwrite=False + ) + yaml_setup.test_but_jury(formsemestre, doc) diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index 2fb61d65..089b1461 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -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 -qui peut donner la formation, son ref. compétences, les formsemestres, -les étudiants et leurs notes. +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_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 @@ -352,7 +390,7 @@ def _check_decisions_ues( decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict] ): """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(): # retrouve l'UE @@ -410,11 +448,18 @@ def _check_decisions_rcues( 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 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. + deca est le résultat calculé par ScoDoc deca_att est un dict lu du YAML """ @@ -435,3 +480,70 @@ def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict): _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)