diff --git a/app/models/etudiants.py b/app/models/etudiants.py index eb9a95821..d4ee28ef0 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -83,6 +83,14 @@ class Identite(db.Model): args = make_etud_args(etudid=etudid, code_nip=code_nip) return Identite.query.filter_by(**args).first_or_404() + @classmethod + def create_etud(cls, **args): + "Crée un étudiant, avec admission et adresse vides." + etud: Identite = cls(**args) + etud.adresses.append(Adresse()) + etud.admission.append(Admission()) + return etud + @property def civilite_str(self): """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, diff --git a/requirements-3.9.txt b/requirements-3.9.txt index 4ca2adc21..9032183ad 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -70,6 +70,7 @@ python-docx==0.8.11 python-dotenv==0.20.0 python-editor==1.0.4 pytz==2022.1 +PyYAML==6.0 redis==4.2.2 reportlab==3.6.9 requests==2.27.1 diff --git a/tests/unit/cursus_but_gb.yaml b/tests/unit/cursus_but_gb.yaml new file mode 100644 index 000000000..458cf81c5 --- /dev/null +++ b/tests/unit/cursus_but_gb.yaml @@ -0,0 +1,61 @@ +# Tests unitaires jury BUT +# Essais avec un BUT GB et deux parcours sur S1, S2, S3 + +FormationFilename: scodoc_formation_BUT_GB_v1.xml +ReferentielCompetencesFilename: but-GB-05012022-081625.xml +ReferentielCompetencesSpecialite: GB + +FormSemestres: + # S1 et S2 avec les parcours séparés: + S1_SEE: + idx: 1 + date_debut: 2021-09-01 + date_fin: 2022-01-15 + codes_parcours: ['SEE'] + S1_BMB: + idx: 1 + date_debut: 2021-09-01 + date_fin: 2022-01-15 + codes_parcours: ['BMB'] + S2_SEE: + idx: 2 + date_debut: 2022-01-15 + date_fin: 2022-06-30 + codes_parcours: ['SEE'] + S2_BMB: + idx: 2 + date_debut: 2022-01-15 + date_fin: 2022-06-30 + codes_parcours: ['BMB'] + # S3 avec les deux parcours réunis: + S3: + idx: 3 + date_debut: 2022-09-01 + date_fin: 2023-01-15 + codes_parcours: ['SEE', 'BMB'] + + +Etudiants: + Aaaaa: + prenom: Étudiant + civilite: M + formsemestres: + S1_SEE: + parcours: SEE + notes_modules: + R1.01: 12 + R1.SEE.11: 15 + S2_SEE: + parcours: SEE + S3: + parcours: SEE + Bbbbb: + prenom: Étudiante + civilite: F + formsemestres: + S1_BMB: + parcours: BMB + S2_BMB: + parcours: BMB + S3: + parcours: BMB diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py new file mode 100644 index 000000000..199e8b1ef --- /dev/null +++ b/tests/unit/test_but_jury.py @@ -0,0 +1,272 @@ +""" Test jury BUT avec parcours +""" +import datetime +import os +from pathlib import Path + +from flask import current_app, g +import pytest +import yaml + +import app +from app import db + +from app.auth.models import User +from app.but.import_refcomp import orebut_import_refcomp +from app.models import ( + ApcParcours, + ApcReferentielCompetences, + Evaluation, + Formation, + FormSemestre, + Identite, + Module, + ModuleImpl, + ModuleUECoef, +) + +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 config import TestConfig + +from tests.conftest import RESOURCES_DIR + +DEPT = TestConfig.DEPT_TEST + + +def setup_but_formation(doc: dict) -> Formation: + """Importe la formation BUT, l'associe au référentiel de compétences. + Après cette fonction, on a une formation chargée, et associée à son ref. comp. + """ + refcomp_filename = doc["ReferentielCompetencesFilename"] + refcomp_specialite = doc["ReferentielCompetencesSpecialite"] + app.set_sco_dept(DEPT) + # Lecture fichier XML local: + with open( + os.path.join(RESOURCES_DIR, "formations", doc["FormationFilename"]), + 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 + # --- 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) + db.session.commit() + return formation + + +def _un_semestre( + formation: Formation, + parcours: list[ApcParcours], + semestre_id: int, + titre: str, + date_debut: str, + date_fin: str, +) -> FormSemestre: + "Création d'un formsemetre" + 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 pour chaque UE une ressource avec un coef vers cette UE + added_ressources = set() + for parcour in parcours + [None]: + for ue in formation.query_ues_parcour(parcour): + ressource = ( + Module.query.filter_by( + formation=formation, + semestre_id=1, + module_type=scu.ModuleType.RESSOURCE, + ) + .join(ModuleUECoef) + .filter_by(ue=ue) + .first() + ) + if ressource is not None: + if ressource.id not in added_ressources: + modimpl = ModuleImpl(module=ressource, responsable_id=a_user.id) + db.session.add(modimpl) + formsemestre.modimpls.append(modimpl) + added_ressources.add(ressource.id) + + # Ajoute la première SAE + sae = formation.modules.filter_by( + semestre_id=1, module_type=scu.ModuleType.SAE + ).first() + modimpl = ModuleImpl(module=sae, responsable_id=a_user.id) + 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: + 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) + # 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""" + 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é: + assert modimpl is not None + for evaluation in modimpl.evaluations: + # s'il y a plusieurs evals, affecte la même note à chacune + sco_saisie_notes.notes_add( + a_user, + evaluation.id, + [(etud.id, float(note))], + comment="note_les_modules", + ) + + +def setup_but_formsemestres(formation: Formation, doc: str): + """Création des formsemestres pour tester les parcours BUT""" + for titre, infos in doc["FormSemestres"].items(): + parcours = [] + for code_parcour in infos["codes_parcours"]: + parcour = formation.referentiel_competence.parcours.filter_by( + code=code_parcour + ).first() + assert parcour is not None + parcours.append(parcour) + _ = _un_semestre( + 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""" + for nom, infos in doc["Etudiants"].items(): + etud = Identite.create_etud( + dept_id=g.scodoc_dept_id, + nom=nom, + prenom=infos["prenom"], + civilite=infos["civilite"], + ) + 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 None: + group_ids = [] + else: + 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] + 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 test_but_jury(test_client): + "Test jurys BUT1, BUT2 avec parcours" + with open("tests/unit/cursus_but_gb.yaml", encoding="utf-8") as f: + doc = yaml.safe_load(f.read()) + + formation = setup_but_formation(doc) + setup_but_formsemestres(formation, doc) + + inscrit_les_etudiants(formation, doc) + + note_les_modules(doc) diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 19659bd6c..996007c8e 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -155,7 +155,7 @@ def create_fake_etud(dept: Departement) -> Identite: """Créé un faux étudiant et l'insère dans la base.""" civilite = random.choice(("M", "F", "X")) nom, prenom = nomprenom(civilite) - etud: Identite = Identite( + etud: Identite = Identite.create_etud( civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id ) db.session.add(etud)