############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ 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_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_formsemestre() - 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 APRES 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 yaml from flask import g from app import db from app.auth.models import User from app.models import ( ApcParcours, DispenseUE, Evaluation, Formation, FormSemestre, Identite, Module, ModuleImpl, UniteEns, ) from app.scodoc import sco_formations from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups 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_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. """ # Lecture fichier XML local: with open( os.path.join(RESOURCES_DIR, "formations", formation_infos["filename"]), encoding="utf-8", ) as f: doc = f.read() # --- Création de la formation formation_id, _, _ = sco_formations.formation_import_xml(doc) formation: Formation = db.session.get(Formation, formation_id) assert formation return formation def create_formsemestre( formation: Formation, parcours: list[ApcParcours], semestre_id: int, titre: str, date_debut: str, date_fin: str, elt_sem_apo: str = None, elt_annee_apo: str = None, etape_apo: str = None, ) -> 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, dept_id=g.scodoc_dept_id, titre=titre, semestre_id=semestre_id, date_debut=date_debut, date_fin=date_fin, elt_sem_apo=elt_sem_apo, elt_annee_apo=elt_annee_apo, ) db.session.add(formsemestre) db.session.flush() # set responsable (list) a_user = User.query.first() formsemestre.responsables = [a_user] formsemestre.add_etape(etape_apo) # Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués sem_parcours_ids = {p.id for p in parcours} modules = [ m for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id) if not m.parcours # module de tronc commun or (not sem_parcours_ids) # semestre sans parcours => tous or ({p.id for p in m.parcours} & sem_parcours_ids) ] for module in modules: modimpl = ModuleImpl(module=module, responsable_id=a_user.id) db.session.add(modimpl) formsemestre.modimpls.append(modimpl) # Crée une évaluation dans chaque module create_evaluations(formsemestre) # Partition par défaut: db.session.commit() partition_id = sco_groups.partition_create( formsemestre.id, default=True, redirect=False ) _ = sco_groups.create_group(partition_id, default=True) # Partition de parcours: if formsemestre.formation.is_apc(): formsemestre.setup_parcours_groups() return formsemestre def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True): """Crée une évaluation dans chaque module du semestre""" for modimpl in formsemestre.modimpls: evaluation = Evaluation( moduleimpl=modimpl, date_debut=formsemestre.date_debut, description=f"Exam {modimpl.module.titre}", coefficient=1.0, note_max=20.0, numero=1, publish_incomplete=publish_incomplete, ) db.session.add(evaluation) 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, formsemestre_titre: str = ""): """Saisie les notes des étudiants pour ce formsemestre doc : données YAML """ a_user = User.query.first() formsemestre: FormSemestre = FormSemestre.query.filter_by( titre=formsemestre_titre ).first() assert formsemestre is not None for nom, infos in doc["Etudiants"].items(): etud: Identite = Identite.query.filter_by(nom=nom).first() assert etud is not None sem_infos = infos["formsemestres"].get(formsemestre_titre) if sem_infos: for code_module, note in sem_infos.get("notes_modules", {}).items(): modimpl = ( formsemestre.modimpls.join(Module) .filter_by(code=code_module) .first() ) # le sem. doit avoir un module du code indiqué: if modimpl is None: raise ValueError(f"module de code '{code_module}' introuvable") for evaluation in modimpl.evaluations: # s'il y a plusieurs evals, affecte la même note à chacune note_value, invalid = sco_saisie_notes.convert_note_from_string( str(note), 20.0, note_min=scu.NOTES_MIN, etudid=etud.id, absents=[], tosuppress=[], invalids=[], ) assert not invalid # valeur note invalide assert isinstance(note_value, float) sco_saisie_notes.notes_add( a_user, evaluation.id, [(etud.id, note_value)], comment="note_les_modules", ) def setup_formsemestre( formation: Formation, doc: str, formsemestre_titre: str = "" ) -> FormSemestre: """Création du formsemestre de titre indiqué. Le cas échéant associé à son parcours. """ infos = doc["FormSemestres"][formsemestre_titre] codes_parcours = infos.get("codes_parcours", []) assert formation.is_apc() or not codes_parcours # parcours seulement en APC 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) elt_sem_apo = infos.get("elt_sem_apo") elt_annee_apo = infos.get("elt_annee_apo") etape_apo = infos.get("etape_apo") formsemestre = create_formsemestre( formation, parcours, infos["idx"], formsemestre_titre, infos["date_debut"], infos["date_fin"], elt_sem_apo=elt_sem_apo, elt_annee_apo=elt_annee_apo, etape_apo=etape_apo, ) db.session.flush() return formsemestre def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""): """Inscrit les étudiants dans chacun de leurs formsemestres""" etudiants = doc.get("Etudiants") if not etudiants: return formsemestre: FormSemestre = FormSemestre.query.filter_by( titre=formsemestre_titre ).first() assert formsemestre is not None partition_parcours = formsemestre.partitions.filter_by( partition_name=scu.PARTITION_PARCOURS ).first() for nom, infos in etudiants.items(): # Création des étudiants (sauf si déjà existants) prenom = infos.get("prenom", "prénom") civilite = infos.get("civilite", "X") code_nip = infos.get("code_nip", None) etud = Identite.query.filter_by( nom=nom, prenom=prenom, civilite=civilite ).first() if etud is None: etud = Identite.create_etud( dept_id=g.scodoc_dept_id, nom=nom, prenom=prenom, civilite=civilite, code_nip=code_nip, ) db.session.add(etud) db.session.commit() # L'inscrire au formsemestre sem_infos = infos["formsemestres"].get(formsemestre_titre) if sem_infos: if partition_parcours is not None and "parcours" in sem_infos: group = partition_parcours.groups.filter_by( group_name=sem_infos["parcours"] ).first() assert group is not None # le groupe de parcours doit exister group_ids = [group.id] else: group_ids = [] # Génère des dispenses d'UEs if "dispense_ues" in sem_infos: etud_dispense_ues(formsemestre, etud, sem_infos["dispense_ues"]) sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( formsemestre.id, etud.id, group_ids=group_ids, etat=scu.INSCRIT, method="tests/unit/inscrit_les_etudiants", ) # Met à jour les inscriptions: formsemestre.update_inscriptions_parcours_from_groups() def etud_dispense_ues( formsemestre: FormSemestre, etud: Identite, ue_acronymes: list[str] ): """Génère des dispenses d'UE""" for ue_acronyme in set(ue_acronymes): ue: UniteEns = formsemestre.formation.ues.filter_by( acronyme=ue_acronyme ).first() assert ue disp = DispenseUE(formsemestre_id=formsemestre.id, ue_id=ue.id, etudid=etud.id) db.session.add(disp) def setup_from_yaml(filename: str) -> tuple[dict, Formation, list[str]]: """Lit le fichier yaml et construit l'ensemble des objets""" with open(filename, encoding="utf-8") as f: doc = yaml.safe_load(f.read()) # Passe tous les noms en majuscules (car stockés ainsi en base) if "Etudiants" in doc: doc["Etudiants"] = {k.upper(): v for (k, v) in doc["Etudiants"].items()} for e in doc["Etudiants"].values(): if "prenom" in e: e["prenom"] = e["prenom"].upper() # Charge de ref. comp. avant la formation, de façon à pouvoir # re-créer les associations UE/Niveaux yaml_setup_but.setup_formation_referentiel(doc.get("ReferentielCompetences", {})) formation = setup_formation(doc["Formation"]) yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"]) formsemestre_titres = list(doc["FormSemestres"].keys()) return doc, formation, formsemestre_titres def create_formsemestre_with_etuds( doc: dict, formation: Formation, formsemestre_titre: str ) -> FormSemestre: """Création formsemestre de titre indiqué, puis inscrit ses étudianst et les note""" formsemestre = setup_formsemestre( formation, doc, formsemestre_titre=formsemestre_titre ) etudiants = doc.get("Etudiants") if etudiants: inscrit_les_etudiants(doc, formsemestre_titre=formsemestre_titre) note_les_modules(doc, formsemestre_titre=formsemestre_titre) return formsemestre