From da7f9a334f1ec6af124c7349f07850112984c446 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 1 Jul 2023 17:42:04 +0200 Subject: [PATCH] =?UTF-8?q?Export=20Apog=C3=A9e:=20setup=20via=20test=20un?= =?UTF-8?q?itaire,=20maquette=20de=20test.=20Corrige=20cas=202=20modules?= =?UTF-8?q?=20avec=20m=C3=AAme=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formsemestre.py | 5 ++ app/scodoc/sco_apogee_csv.py | 16 +++-- app/scodoc/sco_apogee_reader.py | 11 ++- pytest.ini | 1 + tests/ressources/apogee/BUT-INFO-S2.txt | 76 +++++++++++++++++++++ tests/ressources/yaml/cursus_but_info.yaml | 79 ++++++++++++---------- tests/unit/test_apogee_export.py | 54 +++++++++++++++ tests/unit/yaml_setup.py | 16 +++++ 8 files changed, 218 insertions(+), 40 deletions(-) create mode 100644 tests/ressources/apogee/BUT-INFO-S2.txt create mode 100644 tests/unit/test_apogee_export.py diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 64668fa1..6b76e225 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -526,6 +526,11 @@ class FormSemestre(db.Model): return "" return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) + def add_etape(self, etape_apo: str): + "Ajoute une étape" + etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo) + db.session.add(etape) + def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]: """Calcule la liste des regroupements cohérents d'UE impliquant ce formsemestre. diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index b8addd5b..2d3b6af4 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -216,7 +216,12 @@ class ApoEtud(dict): break self.col_elts[code] = elt if elt is None: - self.new_cols[col_id] = self.cols[col_id] + try: + self.new_cols[col_id] = self.cols[col_id] + except KeyError as exc: + raise ScoFormatError( + f"""Fichier Apogee invalide : ligne mal formatée ?
colonne {col_id} non déclarée ?""" + ) from exc else: try: self.new_cols[col_id] = sco_elts[code][ @@ -343,14 +348,17 @@ class ApoEtud(dict): module_code_found = False for modimpl in modimpls: module = modimpl["module"] - if module["code_apogee"] and code in { - x.strip() for x in module["code_apogee"].split(",") - }: + if ( + res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid] + and module["code_apogee"] + and code in {x.strip() for x in module["code_apogee"].split(",")} + ): n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) if n != "NI" and self.export_res_modules: return dict(N=self.fmt_note(n), B=20, J="", R="") else: module_code_found = True + if module_code_found: return VOID_APO_RES # diff --git a/app/scodoc/sco_apogee_reader.py b/app/scodoc/sco_apogee_reader.py index 1e798433..2a56d85d 100644 --- a/app/scodoc/sco_apogee_reader.py +++ b/app/scodoc/sco_apogee_reader.py @@ -295,8 +295,15 @@ class ApoCSVReadWrite: filename=self.get_filename(), ) cols = {} # { col_id : value } - for i, field in enumerate(fields): - cols[self.col_ids[i]] = field + try: + for i, field in enumerate(fields): + cols[self.col_ids[i]] = field + except IndexError as exc: + raise + raise ScoFormatError( + f"Fichier Apogee incorrect (colonnes excédentaires ? ({i}/{field}))", + filename=self.get_filename(), + ) from exc etud_tuples.append( ApoEtudTuple( nip=fields[0], # id etudiant diff --git a/pytest.ini b/pytest.ini index e4d9d0be..e92885fe 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,7 @@ [pytest] markers = slow: marks tests as slow (deselect with '-m "not slow"') + apo but_gb but_gccd but_mlt diff --git a/tests/ressources/apogee/BUT-INFO-S2.txt b/tests/ressources/apogee/BUT-INFO-S2.txt new file mode 100644 index 00000000..1970f0fe --- /dev/null +++ b/tests/ressources/apogee/BUT-INFO-S2.txt @@ -0,0 +1,76 @@ +XX-APO_TITRES-XX +apoC_annee 2021/2022 +apoC_cod_dip DIPTIS2 +apoC_Cod_Exp 2 +apoC_cod_vdi 17 +apoC_Fichier_Exp export.txt +apoC_lib_dip BUT INFO TEST +apoC_Titre1 Maquette pour tests unitaires sur un BUT Info S2 +apoC_Titre2 + +XX-APO_TYP_RES-XX +10 AB1 AB2 ABI ABJ ADM AJ AJRO C1 DEF DIF +18 AB1 AB2 ABI ABJ ADM ADMC ADMD AJ AJAC AJAR AJRO ATT B1 C1 COMP DEF DIF NAR +45 ABI ABJ ADAC ADM ADMC ADMD AIR AJ AJAR AJCP AJRO AJS ATT B1 B2 C1 COMP CRED DEF DES DETT DIF ENER ENRA EXC INFA INFO INST LC MACS N1 N2 NAR NON NSUI NVAL OUI SUIV SUPS TELE TOEF TOIE VAL VALC VALR +10 ABI ABJ ADMC COMP DEF DIS NVAL VAL VALC VALR +AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis AJ : Ajourné AJRO : Ajourné - Réorientation Obligatoire C1 : Niveau C1 DEF : Défaillant DIF : Décision différée +AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AJ : Ajourné AJAC : Ajourné mais accès autorisé à étape sup. AJAR : Ajourné et Admis A Redoubler AJRO : Ajourné - Réorientation Obligatoire ATT : En attente de décison B1 : Niveau B1 C1 : Niveau C1 COMP : Compensé DEF : Défaillant DIF : Décision différée NAR : Ajourné non admis à redoubler +ABI : Absence ABJ : Absence justifiée ADAC : Admis avant choix ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AIR : Ingénieur spécialité Informatique appr AJ : Ajourné AJAR : Ajourné et Admis A Redoubler AJCP : Ajourné mais autorisé à compenser AJRO : Ajourné - Réorientation Obligatoire AJS : Ajourné (note éliminatoire) ATT : En attente de décison B1 : Niveau B1 B2 : Niveau B2 C1 : Niveau C1 COMP : Compensé CRED : Eléments en crédits DEF : Défaillant DES : Désistement DETT : Eléments en dettes DIF : Décision différée ENER : Ingénieur spécialité Energétique ENRA : Ingénieur spécialité Energétique appr EXC : Exclu INFA : Ingénieur spécialité Informatique appr INFO : Ingénieur spécialié Informatique INST : Ingénieur spécialité Instrumentation LC : Liste complémentaire MACS : Ingénieur spécialité MACS N1 : Compétences CLES N2 : Niveau N2 NAR : Ajourné non admis à redoubler NON : Non NSUI : Non suivi(e) NVAL : Non Validé(e) OUI : Oui SUIV : Suivi(e) SUPS : Supérieur au seuil TELE : Ingénieur spéciailté Télécommunications TOEF : TOEFL TOIE : TOEIC VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement +ABI : Absence ABJ : Absence justifiée ADMC : Admis avec compensation COMP : Compensé DEF : Défaillant DIS : Dispense examen NVAL : Non Validé(e) VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement + +XX-APO_COLONNES-XX +apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro +apoL_a02_nom Nom +apoL_a03_prenom Prénom +apoL_a04_naissance Session Admissibilité Naissance +APO_COL_VAL_DEB +apoL_c0001 ELP V1INFU21 2021 0 1 N V1INFU21 - UE 2.1 Réaliser 0 1 Note +apoL_c0002 ELP V1INFU21 2021 0 1 B 0 1 Barème +apoL_c0003 ELP V1INFU21 2021 0 1 J 0 1 Pts Jury +apoL_c0004 ELP V1INFU21 2021 0 1 R 0 1 Résultat +apoL_c0005 ELP V1INFU22 2021 0 1 N V1INFU22 - UE 2.2 Optimiser 0 1 Note +apoL_c0006 ELP V1INFU22 2021 0 1 B 0 1 Barème +apoL_c0007 ELP V1INFU22 2021 0 1 J 0 1 Pts Jury +apoL_c0008 ELP V1INFU22 2021 0 1 R 0 1 Résultat +apoL_c0009 ELP V1INFU23 2021 0 1 N V1INFU23 - UE 2.3 Administrer 0 1 Note +apoL_c0010 ELP V1INFU23 2021 0 1 B 0 1 Barème +apoL_c0011 ELP V1INFU23 2021 0 1 J 0 1 Pts Jury +apoL_c0012 ELP V1INFU23 2021 0 1 R 0 1 Résultat +apoL_c0013 ELP V1INFU24 2021 0 1 N V1INFU24 - UE 2.4 Gérer 0 1 Note +apoL_c0014 ELP V1INFU24 2021 0 1 B 0 1 Barème +apoL_c0015 ELP V1INFU24 2021 0 1 J 0 1 Pts Jury +apoL_c0016 ELP V1INFU24 2021 0 1 R 0 1 Résultat +apoL_c0017 ELP V1INFU25 2021 0 1 N V1INFU25 - UE 2.5 Conduire 0 1 Note +apoL_c0018 ELP V1INFU25 2021 0 1 B 0 1 Barème +apoL_c0019 ELP V1INFU25 2021 0 1 J 0 1 Pts Jury +apoL_c0020 ELP V1INFU25 2021 0 1 R 0 1 Résultat +apoL_c0021 ELP V1INFU26 2021 0 1 N V1INFU26 - UE 2.6 Travailler 0 1 Note +apoL_c0022 ELP V1INFU26 2021 0 1 B 0 1 Barème +apoL_c0023 ELP V1INFU26 2021 0 1 J 0 1 Pts Jury +apoL_c0024 ELP V1INFU26 2021 0 1 R 0 1 Résultat +apoL_c0025 ELP VINFR201 2021 0 1 N VINFR201 - Développement orienté objets 0 1 Note +apoL_c0026 ELP VINFR201 2021 0 1 B 0 1 Barème +apoL_c0027 ELP VINFR207 2021 0 1 N VINFR207 - Graphes 0 1 Note +apoL_c0028 ELP VINFR207 2021 0 1 B 0 1 Barème +apoL_c0029 ELP VINFPOR2 2021 0 1 N VINFPOR2 - Portfolio 0 1 Note +apoL_c0030 ELP VINFPOR2 2021 0 1 B 0 1 Barème +apoL_c0031 ELP TIRW2 2021 0 1 N TIRW2 - Semestre 2 BUT INFO 2 0 1 Note +apoL_c0032 ELP TIRW2 2021 0 1 B 0 1 Barème +apoL_c0033 ELP TIRW2 2021 0 1 J 0 1 Pts Jury +apoL_c0034 ELP TIRW2 2021 0 1 R 0 1 Résultat +apoL_c0035 ELP TIRO 2021 0 1 N TIRO - Année BUT 1 RT 0 1 Note +apoL_c0036 ELP TIRO 2021 0 1 B 0 1 Barème +apoL_c0037 VET TI1 117 2021 0 1 N TI1 - BUT INFO an1 0 1 Note +apoL_c0038 VET TI1 117 2021 0 1 B 0 1 Barème +apoL_c0039 VET TI1 117 2021 0 1 J 0 1 Pts Jury +apoL_c0040 VET TI1 117 2021 0 1 R 0 1 Résultat +APO_COL_VAL_FIN +apoL_c0041 APO_COL_VAL_FIN + +XX-APO_VALEURS-XX +apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029 apoL_c0030 apoL_c0031 apoL_c0032 apoL_c0033 apoL_c0034 apoL_c0035 apoL_c0036 apoL_c0037 apoL_c0038 apoL_c0039 apoL_c0040 + +1001 ex_a1 Jean 10/01/2003 +1002 ex_a2 Lucie 11/01/2003 +1003 ex_b1 Hélène 11/01/2003 +1004 ex_b2 Rose 11/01/2003 diff --git a/tests/ressources/yaml/cursus_but_info.yaml b/tests/ressources/yaml/cursus_but_info.yaml index f735e1e5..52ab3759 100644 --- a/tests/ressources/yaml/cursus_but_info.yaml +++ b/tests/ressources/yaml/cursus_but_info.yaml @@ -1,4 +1,4 @@ -# Tests unitaires +# Tests unitaires # Le BUT Info a 4 parcours qui partagent certains niveaux de compétences # mais à ces niveaux sont associés des UEs dont les coefficients des ressources # varient selon le parcours. @@ -14,58 +14,58 @@ Formation: # nota: les associations UE/Niveaux sont déjà données dans ce fichier XML. ues: # S1 - 'UE11': + "UE11": annee: BUT1 - 'UE12': + "UE12": annee: BUT1 - 'UE13': + "UE13": annee: BUT1 - 'UE14': + "UE14": annee: BUT1 - 'UE15': + "UE15": annee: BUT1 - 'UE16': + "UE16": annee: BUT1 # S2 - 'UE21': + "UE21": annee: BUT1 - 'UE22': + "UE22": annee: BUT1 - 'UE23': + "UE23": annee: BUT1 - 'UE24': + "UE24": annee: BUT1 - 'UE25': + "UE25": annee: BUT1 - 'UE26': + "UE26": annee: BUT1 # S3 - 'UE31': + "UE31": annee: BUT2 - 'UE32': + "UE32": annee: BUT2 - 'UE33': + "UE33": annee: BUT2 - 'UE34': + "UE34": annee: BUT2 - 'UE35': + "UE35": annee: BUT2 - 'UE36': + "UE36": annee: BUT2 # S4 - 'UE41-A': # UE pour le parcours A + "UE41-A": # UE pour le parcours A annee: BUT2 - 'UE41-B': # UE pour le parcours B (même contenu, coefs différents) + "UE41-B": # UE pour le parcours B (même contenu, coefs différents) annee: BUT2 - 'UE42': + "UE42": annee: BUT2 - 'UE43': + "UE43": annee: BUT2 - 'UE44': + "UE44": annee: BUT2 - 'UE45': + "UE45": annee: BUT2 - 'UE46': + "UE46": annee: BUT2 FormSemestres: @@ -74,37 +74,41 @@ FormSemestres: idx: 1 date_debut: 2021-09-01 date_fin: 2022-01-15 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] S2: idx: 2 date_debut: 2022-01-16 date_fin: 2022-06-30 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] + elt_sem_apo: TIRW2 + elt_annee_apo: TIRO + etape_apo: TI1!117 S3: idx: 3 date_debut: 2022-09-01 date_fin: 2023-01-15 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] S4: idx: 4 date_debut: 2023-01-16 date_fin: 2023-06-30 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] S5: idx: 5 date_debut: 2023-09-01 date_fin: 2024-01-15 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] S6: idx: 6 date_debut: 2024-01-16 date_fin: 2024-06-30 - codes_parcours: ['A', 'B'] + codes_parcours: ["A", "B"] Etudiants: ex_a1: # cursus S1 -> S6, valide tout prenom: Jean civilite: M + code_nip: 1001 formsemestres: # on ne note que le portfolio, qui affecte toutes les UEs S1: @@ -115,6 +119,7 @@ Etudiants: parcours: A notes_modules: "P2": 12 + "R2.04-A": 16 S3: parcours: A notes_modules: @@ -135,6 +140,7 @@ Etudiants: ex_a2: # cursus S1 -> S6, valide tout sauf S5 prenom: Lucie civilite: F + code_nip: 1002 formsemestres: # on ne note que le portfolio, qui affecte toutes les UEs S1: @@ -145,6 +151,7 @@ Etudiants: parcours: A notes_modules: "P2": 12 + "R2.04-A": 17 S3: parcours: A notes_modules: @@ -161,10 +168,11 @@ Etudiants: parcours: A notes_modules: "P6-A": 16 - + ex_b1: # cursus S1 -> S6, valide tout prenom: Hélène civilite: F + code_nip: 1003 formsemestres: # on ne note que le portfolio, qui affecte toutes les UEs S1: @@ -175,6 +183,7 @@ Etudiants: parcours: B notes_modules: "P2": 12 + "R2.04-B": 18 S3: parcours: B notes_modules: @@ -191,10 +200,11 @@ Etudiants: parcours: B notes_modules: "P6-B": 16 - + ex_b2: # cursus S1 -> S6, valide tout sauf S6 prenom: Rose civilite: F + code_nip: 1004 formsemestres: # on ne note que le portfolio, qui affecte toutes les UEs S1: @@ -205,6 +215,7 @@ Etudiants: parcours: B notes_modules: "P2": 12 + "R2.04-B": 19 S3: parcours: B notes_modules: diff --git a/tests/unit/test_apogee_export.py b/tests/unit/test_apogee_export.py new file mode 100644 index 00000000..4b84f0a8 --- /dev/null +++ b/tests/unit/test_apogee_export.py @@ -0,0 +1,54 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" Test export Apogéee + +Ces tests sont généralement lents (construction de la base), +et donc marqués par `@pytest.mark.slow`. + +Certains sont aussi marqués par @pytest.mark.lemans ou @pytest.mark.lyon +pour lancer certains tests spécifiques seulement. + +Exemple utilisation spécifique: +# test sur "apo" seulement: +pytest --pdb -m apo tests/unit/test_apogee_export.py + +Elements Apogée simulés: + +- UEs : TIU2x +- Ressources: R2.xy : TIRxy (VRETR201 -> TIR201) +""" + +import pytest +from tests.unit import yaml_setup, yaml_setup_but + +import app +from app.but.jury_but_validation_auto import formsemestre_validation_auto_but +from app.models import Formation, FormSemestre, UniteEns +from config import TestConfig + +DEPT = TestConfig.DEPT_TEST + + +@pytest.mark.skip # Ce "test" est utilisé comme setup pour développer, pas comme test unitaire routinier +@pytest.mark.slow +@pytest.mark.apo +def test_refcomp_niveaux_info(test_client): + """Test niveaux / parcours / UE pour un BUT INFO + avec parcours A et B, même compétences mais coefs différents + selon le parcours. + """ + # WIP + # pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4. + app.set_sco_dept(DEPT) + doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml( + "tests/ressources/yaml/cursus_but_info.yaml" + ) + for formsemestre_titre in formsemestre_titres: + formsemestre = yaml_setup.create_formsemestre_with_etuds( + doc, formation, formsemestre_titre + ) + # diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index bf4dc70e..277728e4 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -99,6 +99,9 @@ def create_formsemestre( 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 @@ -110,11 +113,15 @@ def create_formsemestre( semestre_id=semestre_id, date_debut=date_debut, date_fin=date_fin, + elt_sem_apo=elt_sem_apo, + elt_annee_apo=elt_annee_apo, ) # set responsable (list) a_user = User.query.first() formsemestre.responsables = [a_user] db.session.add(formsemestre) + db.session.flush() + 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 = [ @@ -228,6 +235,10 @@ def setup_formsemestre( 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, @@ -235,6 +246,9 @@ def setup_formsemestre( 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() @@ -257,6 +271,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""): # 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() @@ -266,6 +281,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""): nom=nom, prenom=prenom, civilite=civilite, + code_nip=code_nip, ) db.session.add(etud) db.session.commit()