############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 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_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 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 = Formation.query.get(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, ) -> 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, ) # set responsable (list) a_user = User.query.first() formsemestre.responsables = [a_user] db.session.add(formsemestre) # 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) 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): """Crée une évaluation dans chaque module du semestre""" for modimpl in formsemestre.modimpls: evaluation = Evaluation( moduleimpl=modimpl, jour=formsemestre.date_debut, description=f"Exam {modimpl.module.titre}", coefficient=1.0, note_max=20.0, numero=1, ) 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): """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() assert etud is not None for titre, sem_infos in infos["formsemestres"].items(): formsemestre: FormSemestre = FormSemestre.query.filter_by( titre=titre ).first() assert formsemestre is not None 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_formsemestres(formation: Formation, doc: str): """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 codes_parcours: parcour = formation.referentiel_competence.parcours.filter_by( code=code_parcour ).first() assert parcour is not None parcours.append(parcour) _ = create_formsemestre( formation, parcours, infos["idx"], titre, infos["date_debut"], infos["date_fin"], ) db.session.flush() assert FormSemestre.query.count() == len(doc["FormSemestres"]) def inscrit_les_etudiants(formation: Formation, doc: dict): """Inscrit les étudiants dans chacun de leurs formsemestres""" etudiants = doc.get("Etudiants") if not etudiants: return for nom, infos in etudiants.items(): etud = Identite.create_etud( dept_id=g.scodoc_dept_id, nom=nom, prenom=infos.get("prenom", "prénom"), civilite=infos.get("civilite", "X"), ) db.session.add(etud) db.session.commit() # L'inscrire à ses formsemestres for titre, sem_infos in infos["formsemestres"].items(): formsemestre: FormSemestre = FormSemestre.query.filter_by( titre=titre ).first() assert formsemestre is not None partition_parcours = formsemestre.partitions.filter_by( partition_name=scu.PARTITION_PARCOURS ).first() 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: for formsemestre in formation.formsemestres: 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) -> dict: """Lit le fichier yaml et construit l'ensemble des objets""" with open(filename, encoding="utf-8") as f: doc = yaml.safe_load(f.read()) # 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"]) setup_formsemestres(formation, doc) etudiants = doc.get("Etudiants") if etudiants: inscrit_les_etudiants(formation, doc) note_les_modules(doc) return doc