############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Résultats semestres BUT """ import time import numpy as np import pandas as pd from flask import g, url_for from app import log from app.comp import moy_ue, moy_sem, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_codes_parcours from app.scodoc.sco_codes_parcours import UE_SPORT import app.scodoc.sco_utils as scu class ResultatsSemestreBUT(NotesTableCompat): """Résultats BUT: organisation des calculs""" _cached_attrs = NotesTableCompat._cached_attrs + ( "modimpl_coefs_df", "modimpls_evals_poids", "sem_cube", ) def __init__(self, formsemestre): super().__init__(formsemestre) self.modimpl_coefs_df = None """DataFrame, row UEs(sans bonus), cols modimplid, value coef""" self.sem_cube = None """ndarray (etuds x modimpl x ue)""" if not self.load_cached(): t0 = time.time() self.compute() t1 = time.time() self.store() t2 = time.time() log( f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)" ) def compute(self): "Charge les notes et inscriptions et calcule les moyennes d'UE et gen." ( self.sem_cube, self.modimpls_evals_poids, self.modimpls_results, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.formsemestre, modimpls=self.formsemestre.modimpls_sorted ) # l'idx de la colonne du mod modimpl.id est # modimpl_coefs_df.columns.get_loc(modimpl.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) # Masque de tous les modules _sauf_ les bonus (sport) modimpls_mask = [ modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, self.formsemestre.modimpls_sorted, self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df, modimpls_mask, ) # Les coefficients d'UE ne sont pas utilisés en APC self.etud_coef_ue_df = pd.DataFrame( 0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns ) # --- Modules de MALUS sur les UEs self.malus = moy_ue.compute_malus( self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df ) self.etud_moy_ue -= self.malus # --- Bonus Sport & Culture if not all(modimpls_mask): # au moins un module bonus bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: bonus: BonusSport = bonus_class( self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df.transpose(), self.etud_moy_gen, self.etud_moy_ue, ) self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: self.etud_moy_ue += self.bonus_ues # somme les dataframes # Clippe toutes les moyennes d'UE dans [0,20] self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) # Moyenne générale indicative: # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs( # self.etud_moy_ue, self.modimpl_coefs_df # ) self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects( self.etud_moy_ue, [ue.ects for ue in self.ues if ue.type != UE_SPORT], formation_id=self.formsemestre.formation_id, skip_empty_ues=sco_preferences.get_preference( "but_moy_skip_empty_ues", self.formsemestre.id ), ) # --- UE capitalisées self.apply_capitalisation() # --- Classements: self.compute_rangs() def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl En APC, il s'agit d'une moyenne indicative sans valeur. Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM) """ mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id) etud_idx = self.etud_index[etudid] # moyenne sur les UE: if len(self.sem_cube[etud_idx, mod_idx]): return np.nanmean(self.sem_cube[etud_idx, mod_idx]) return np.nan def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: """Détermine le coefficient de l'UE pour cet étudiant. N'est utilisé que pour l'injection des UE capitalisées dans la moyenne générale. En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE. (ne dépend pas des modules auxquels est inscrit l'étudiant, ). """ return self.modimpl_coefs_df.loc[ue.id].sum() def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl ayant des coefs non nuls vers cette UE et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant. """ # sert pour l'affichage ou non de l'UE sur le bulletin et la table recap coefs = self.modimpl_coefs_df # row UE, cols modimpl modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted if (coefs[modimpl.id][ue_id] != 0) and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: return [ modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT ] return modimpls def get_table_moyennes_triees(self, convert_values=False) -> list: """Result: tuple avec - rows: liste de dicts { column_id : value } - titles: { column_id : title } - columns_ids: (liste des id de colonnes) . Si convert_values, transforme les notes en chaines ("12.34"). Les colonnes générées sont: etudid rang : rang indicatif (basé sur moy gen) moy_gen : moy gen indicative moy_ue_, ..., les moyennes d'UE moy_res__, ... les moyennes de ressources dans l'UE moy_sae__, ... les moyennes de SAE dans l'UE On ajoute aussi des attributs: - pour les lignes: _css_row_class (inutilisé pour le monent) __class classe css: - la moyenne générale a la classe col_moy_gen - les colonnes SAE ont la classe col_sae - les colonnes Resources ont la classe col_res - les colonnes d'UE ont la classe col_ue - les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_ __order : clé de tri """ def fmt_note(x): return scu.fmt_note(x) if convert_values else x barre_moy = ( self.formsemestre.formation.get_parcours().BARRE_MOY - scu.NOTES_TOLERANCE ) barre_valid_ue = self.formsemestre.formation.get_parcours().NOTES_BARRE_VALID_UE NO_NOTE = "-" # contenu des cellules sans notes rows = [] titles = {"rang": "Rg"} # column_id : title def add_cell( row: dict, col_id: str, title: str, content: str, classes: str = "" ): "Add a row to our table. classes is a list of css class names" row[col_id] = content if classes: row[f"_{col_id}_class"] = classes if not col_id in titles: titles[col_id] = title if classes: titles[f"_{col_id}_class"] = classes etuds_inscriptions = self.formsemestre.etuds_inscriptions ues = self.formsemestre.query_ues(with_sport=True) # avec bonus modimpl_ids = set() # modimpl effectivement présents dans la table for etudid in etuds_inscriptions: etud = Identite.query.get(etudid) row = {"etudid": etudid} # --- Rang add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang") row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}" # --- Identité étudiant add_cell(row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail") add_cell(row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail") add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail") add_cell(row, "nom_short", "Nom", etud.nom_short, "identite_court") row["_nom_short_target"] = url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid, ) row[f"_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"' row["_nom_disp_target"] = row["_nom_short_target"] row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"] self._recap_etud_groups_infos(etudid, row, titles) # --- Moyenne générale moy_gen = self.etud_moy_gen.get(etudid, False) note_class = "" if moy_gen is False: moy_gen = NO_NOTE elif isinstance(moy_gen, float) and moy_gen < barre_moy: note_class = " moy_inf" add_cell( row, "moy_gen", "Moy", fmt_note(moy_gen), "col_moy_gen" + note_class, ) # --- Moyenne d'UE for ue in [ue for ue in ues if ue.type != UE_SPORT]: ue_status = self.get_etud_ue_status(etudid, ue.id) if ue_status is not None: col_id = f"moy_ue_{ue.id}" val = ue_status["moy"] note_class = "" if isinstance(val, float): if val < barre_moy: note_class = " moy_inf" elif val >= barre_valid_ue: note_class = " moy_ue_valid" add_cell( row, col_id, ue.acronyme, fmt_note(val), "col_ue" + note_class, ) # Les moyennes des ressources et SAÉs dans cette UE for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): if ue_status["is_capitalized"]: val = "-c-" else: modimpl_results = self.modimpls_results.get(modimpl.id) if modimpl_results: # pas bonus moys_vers_ue = modimpl_results.etuds_moy_module.get( ue.id ) val = ( moys_vers_ue.get(etudid, "?") if moys_vers_ue is not None else "" ) else: val = "" col_id = ( f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" ) add_cell( row, col_id, modimpl.module.code, fmt_note(val), # class col_res mod_ue_123 f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", ) modimpl_ids.add(modimpl.id) rows.append(row) # tri par rang croissant rows.sort(key=lambda e: e["_rang_order"]) # INFOS POUR FOOTER bottom_infos = self._recap_bottom_infos( [ue for ue in ues if ue.type != UE_SPORT], modimpl_ids, fmt_note ) # --- TABLE FOOTER: ECTS, moyennes, min, max... footer_rows = [] for bottom_line in bottom_infos: row = bottom_infos[bottom_line] # Cases vides à styler: row["moy_gen"] = row.get("moy_gen", "") row["_moy_gen_class"] = "col_moy_gen" # titre de la ligne: row["prenom"] = row["nom_short"] = bottom_line.capitalize() row["_tr_class"] = bottom_line.lower() footer_rows.append(row) return ( rows, footer_rows, titles, [title for title in titles if not title.startswith("_")], ) def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: """Les informations à mettre en bas de la table: min, max, moy, ECTS""" bottom_infos = { # { key : row } avec key = min, max, moy, coef "min": {}, "max": {}, "moy": {}, "coef": {}, } # --- ECTS row = {} for ue in ues: row[f"moy_ue_{ue.id}"] = ue.ects row[f"_moy_ue_{ue.id}_class"] = "col_ue" # style cases vides pour borders verticales bottom_infos["coef"][f"moy_ue_{ue.id}"] = "" bottom_infos["coef"][f"_moy_ue_{ue.id}_class"] = "col_ue" row["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]) row["_moy_gen_class"] = "col_moy_gen" bottom_infos["ects"] = row # --- MIN, MAX, MOY row_min, row_max, row_moy = {}, {}, {} row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min()) row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max()) row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean()) for ue in [ue for ue in ues if ue.type != UE_SPORT]: col_id = f"moy_ue_{ue.id}" row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min()) row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max()) row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean()) row_min[f"_{col_id}_class"] = "col_ue" row_max[f"_{col_id}_class"] = "col_ue" row_moy[f"_{col_id}_class"] = "col_ue" for modimpl in self.formsemestre.modimpls_sorted: if modimpl.id in modimpl_ids: col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" bottom_infos["coef"][col_id] = fmt_note( self.modimpl_coefs_df[modimpl.id][ue.id] ) i = self.modimpl_coefs_df.columns.get_loc(modimpl.id) j = self.modimpl_coefs_df.index.get_loc(ue.id) notes = self.sem_cube[:, i, j] row_min[col_id] = fmt_note(np.nanmin(notes)) row_max[col_id] = fmt_note(np.nanmax(notes)) row_moy[col_id] = fmt_note(np.nanmean(notes)) bottom_infos["min"] = row_min bottom_infos["max"] = row_max bottom_infos["moy"] = row_moy return bottom_infos def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): """Ajoute à row les colonnes sur les groupes pour etud""" # XXX à remonter dans res_common # dec = self.get_etud_decision_sem(etudid) # if dec: # codes_nb[dec["code"]] += 1 row_class = "" etud_etat = self.get_etud_etat(etudid) if etud_etat == sco_codes_parcours.DEM: gr_name = "Dém." row_class = "dem" elif etud_etat == sco_codes_parcours.DEF: gr_name = "Déf." row_class = "def" else: # XXX probablement à revoir pour utiliser données cachées, # via get_etud_groups_in_partition ou autre group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id) gr_name = group["group_name"] or "" row["group"] = gr_name row["_group_class"] = "group" if row_class: row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class]) titles["group"] = "Gr"