diff --git a/sco_version.py b/sco_version.py index 6561608c..b38f10a8 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.40" +SCOVERSION = "9.4.41" SCONAME = "ScoDoc" diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index fec4976c..e4ea40e0 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -19,7 +19,7 @@ pytest --pdb -m lyon tests/unit/test_but_jury.py """ import pytest -from tests.unit import yaml_setup +from tests.unit import yaml_setup, yaml_setup_but import app 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: 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 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: 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() - yaml_setup.test_but_jury(S2, doc) + yaml_setup_but.but_test_jury(S2, doc) 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) @@ -77,7 +77,7 @@ def test_but_jury_GMP_lm(test_client): # Vérifie les deca de tous les semestres: 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à for formsemestre in formsemestres: @@ -85,7 +85,7 @@ def test_but_jury_GMP_lm(test_client): # Vérifie résultats attendus: for formsemestre in formsemestres: - yaml_setup.test_but_jury(formsemestre, doc) + yaml_setup_but.but_test_jury(formsemestre, doc) @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: 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" # et vérification des résultats attendus: @@ -110,4 +110,4 @@ def test_but_jury_GEII_lyon(test_client): formsemestre_validation_auto_but( formsemestre, only_adm=False, no_overwrite=False ) - yaml_setup.test_but_jury(formsemestre, doc) + yaml_setup_but.but_test_jury(formsemestre, doc) diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index 99729846..4d4c9257 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -13,7 +13,7 @@ Le traitement est effectué dans l'ordre suivant: setup_from_yaml() - - setup_but_formation(): + - setup_formation(): - import de la formation (le test utilise une seule formation) - associe_ues_et_parcours(): - crée les associations formation <-> référentiel de compétence @@ -43,33 +43,22 @@ et donc marqués par `@pytest.mark.slow`. """ import os -from pathlib import Path -import re import yaml -from flask import current_app, g +from flask import g from app import db 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 ( - ApcNiveau, ApcParcours, - ApcReferentielCompetences, Evaluation, Formation, FormSemestre, Identite, Module, ModuleImpl, - ModuleUECoef, - UniteEns, ) 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 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 formation_infos["filename"]. 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 -def setup_formation_referentiel(formation: Formation, refcomp_infos: dict): - """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( +def create_formsemestre( formation: Formation, parcours: list[ApcParcours], semestre_id: int, @@ -195,7 +98,8 @@ def _un_semestre( date_debut: str, date_fin: str, ) -> 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( formation=formation, parcours=parcours, @@ -230,8 +134,10 @@ def _un_semestre( formsemestre.id, default=True, redirect=False ) _ = sco_groups.create_group(partition_id, default=True) + # Partition de parcours: - formsemestre.setup_parcours_groups() + if formsemestre.formation.is_apc(): + formsemestre.setup_parcours_groups() return formsemestre @@ -248,13 +154,17 @@ def create_evaluations(formsemestre: FormSemestre): numero=1, ) db.session.add(evaluation) - # Affecte les mêmes poids que les coefs APC: - ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef } - evaluation.set_ue_poids_dict(ue_coef_dict) + + if formsemestre.formation.is_apc(): + # Affecte les mêmes poids que les coefs APC: + ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef } + evaluation.set_ue_poids_dict(ue_coef_dict) def note_les_modules(doc: dict): - """Saisie les notes des étudiants""" + """Saisie les notes des étudiants + doc : données YAML + """ a_user = User.query.first() for nom, infos in doc["Etudiants"].items(): 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): - """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(): + codes_parcours = infos.get("codes_parcours", []) + assert formation.is_apc() or not codes_parcours # parcours seulement en APC parcours = [] - for code_parcour in infos.get("codes_parcours", []): + for code_parcour in codes_parcours: parcour = formation.referentiel_competence.parcours.filter_by( code=code_parcour ).first() assert parcour is not None parcours.append(parcour) - _ = _un_semestre( + + _ = create_formsemestre( formation, parcours, infos["idx"], @@ -362,193 +277,12 @@ def setup_from_yaml(filename: str) -> dict: with open(filename, encoding="utf-8") as f: doc = yaml.safe_load(f.read()) - formation = setup_but_formation(doc["Formation"]) - setup_formation_referentiel(formation, doc.get("ReferentielCompetences", {})) - associe_ues_et_parcours(formation, doc["Formation"]) + formation = setup_formation(doc["Formation"]) + yaml_setup_but.setup_formation_referentiel( + formation, doc.get("ReferentielCompetences", {}) + ) + yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"]) setup_formsemestres(formation, doc) inscrit_les_etudiants(formation, doc) note_les_modules(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) diff --git a/tests/unit/yaml_setup_but.py b/tests/unit/yaml_setup_but.py new file mode 100644 index 00000000..c11e5828 --- /dev/null +++ b/tests/unit/yaml_setup_but.py @@ -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)