diff --git a/app/comp/res_but.py b/app/comp/res_but.py index c65fabc750..00fd9fb760 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -16,6 +16,7 @@ from app.comp.res_compat import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.moduleimpls import ModuleImpl +from app.models.but_refcomp import ApcParcours, ApcNiveau from app.models.ues import DispenseUE, UniteEns from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.scodoc import sco_preferences @@ -41,6 +42,8 @@ class ResultatsSemestreBUT(NotesTableCompat): """ndarray (etuds x modimpl x ue)""" self.etuds_parcour_id = None """Parcours de chaque étudiant { etudid : parcour_id }""" + self.ues_ids_by_parcour: dict[set[int]] = {} + """{ parcour_id : set }, ue_id de chaque parcours""" if not self.load_cached(): t0 = time.time() @@ -259,10 +262,43 @@ class ResultatsSemestreBUT(NotesTableCompat): def etud_ues_ids(self, etudid: int) -> list[int]: """Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus). (surchargée ici pour prendre en compte les parcours) + Ne prend pas en compte les éventuelles DispenseUE (pour le moment ?) """ s = self.ues_inscr_parcours_df.loc[etudid] return s.index[s.notna()] + def etud_parcours_ues_ids(self, etudid: int) -> set[int]: + """Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu + du parcours dans lequel il est inscrit. + Se base sur le parcours dans ce semestre, et le référentiel de compétences. + Note: il n'est pas nécessairement inscrit à toutes ces UEs. + Ensemble vide si pas de référentiel. + + La requête est longue, les ue_ids par parcour sont donc cachés. + """ + parcour_id = self.etuds_parcour_id[etudid] + if parcour_id in self.ues_ids_by_parcour: # cache + return self.ues_ids_by_parcour[parcour_id] + # Hors cache: + ref_comp = self.formsemestre.formation.referentiel_competence + if ref_comp is None: + return set() + parcour: ApcParcours = ApcParcours.query.get(parcour_id) + annee = (self.formsemestre.semestre_id + 1) // 2 + niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp) + # Les UEs du formsemestre associées à ces niveaux: + ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour) + ues_ids = set() + for niveau in niveaux: + ue = ues_parcour.filter_by(niveau_competence=niveau).first() + if ue: + ues_ids.add(ue.id) + + # memoize + self.ues_ids_by_parcour[parcour_id] = ues_ids + + return ues_ids + def etud_has_decision(self, etudid): """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre. prend aussi en compte les autorisations de passage. diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 0d65e9514e..850d02f328 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -125,6 +125,13 @@ class ResultatsSemestre(ResultatsCache): # car tous les étudiants sont inscrits à toutes les UE return [ue.id for ue in self.ues if ue.type != UE_SPORT] + def etud_parcours_ues_ids(self, etudid: int) -> set[int]: + """Ensemble des UEs que l'étudiant "doit" valider. + En formations classiques, c'est la même chose (en set) que etud_ues_ids. + Surchargée en BUT pour donner les UEs du parcours de l'étudiant. + """ + return {ue.id for ue in self.ues if ue.type != UE_SPORT} + def etud_ues(self, etudid: int) -> Generator[UniteEns]: """Liste des UE auxquelles l'étudiant est inscrit (sans bonus, en BUT prend en compte le parcours de l'étudiant).""" diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 0a3ec15887..d23ee313af 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -214,7 +214,6 @@ class ApcReferentielCompetences(db.Model, XMLModel): """ parcours_info = {} for parcour in self.parcours: - print(f"# Parcours {parcour.code}") descr_parcour = {} parcours_info[parcour.id] = descr_parcour for annee in (1, 2, 3): diff --git a/app/tables/recap.py b/app/tables/recap.py index ca55a10196..ded33d0ea5 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -676,7 +676,7 @@ class RowRecap(tb.Row): # Les moyennes des modules (ou ressources et SAÉs) dans cette UE self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"]) - self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id)) + self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id)) ue_valid_txt = ( ue_valid_txt_html ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml index fe52f10b1a..109c9969d9 100644 --- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml +++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml @@ -79,15 +79,44 @@ Formation: # S5 Parcours BAT + TP 'UE5.1': # Parcours BAT seulement annee: BUT3 + parcours: BAT # + RAPEB, BEC competence: "Solutions Bâtiment" 'UE5.2': # Parcours TP seulement annee: BUT3 + parcours: TP # + BEC competence: "Solutions TP" + 'UE5.3': + annee: BUT3 + parcours: RAPEB # + BEC + competence: "Dimensionner" 'UE5.4': annee: BUT3 + parcours: BAT # + TP competence: Organiser 'UE5.5': annee: BUT3 + parcours: BAT # + TP + competence: Piloter + # S6 Parcours BAT + TP + 'UE6.1': # Parcours BAT seulement + annee: BUT3 + parcours: BAT # + RAPEB, BEC + competence: "Solutions Bâtiment" + 'UE6.2': # Parcours TP seulement + annee: BUT3 + parcours: TP # + BEC + competence: "Solutions TP" + 'UE6.3': + annee: BUT3 + parcours: RAPEB # + BEC + competence: "Dimensionner" + 'UE6.4': + annee: BUT3 + parcours: BAT # + TP + competence: Organiser + 'UE6.5': + annee: BUT3 + parcours: BAT # + TP competence: Piloter modules_parcours: @@ -157,7 +186,7 @@ Etudiants: S5: parcours: BAT - dispense_ues: ['UE5.2'] + dispense_ues: ['UE5.2', 'UE5.3'] notes_modules: "R5.01": 15 # toutes UE "SAÉ 5.BAT.01": 10 # UE5.1 @@ -188,7 +217,7 @@ Etudiants: S5: parcours: TP - dispense_ues: ['UE5.1'] + dispense_ues: ['UE5.1', 'UE5.3'] notes_modules: "R5.01": 15 # toutes UE "SAÉ 5.BAT.01": 10 # UE5.1 diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index 2b41dd5706..218638e6c0 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -24,7 +24,7 @@ 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 FormSemestre +from app.models import Formation, FormSemestre from config import TestConfig DEPT = TestConfig.DEPT_TEST @@ -124,3 +124,23 @@ def test_but_jury_GCCD_CY(test_client): formsemestres = FormSemestre.query.order_by( FormSemestre.date_debut, FormSemestre.semestre_id ).all() + + formation: Formation = formsemestres[0].formation + # Vérifie les UEs du parcours BAT + parcour_BAT = formation.referentiel_competence.parcours.filter_by( + code="BAT" + ).first() + assert parcour_BAT + # check le nombre d'UE dans chaque semestre BUT: + assert [ + len(formation.query_ues_parcour(parcour_BAT).filter_by(semestre_idx=i).all()) + for i in range(1, 7) + ] == [5, 5, 5, 5, 3, 3] + # Vérifie les UEs du parcours TP + parcour_TP = formation.referentiel_competence.parcours.filter_by(code="TP").first() + assert parcour_TP + # check le nombre d'UE dans chaque semestre BUT: + assert [ + len(formation.query_ues_parcour(parcour_TP).filter_by(semestre_idx=i).all()) + for i in range(1, 7) + ] == [5, 5, 5, 5, 3, 3]