From 9596c8cf6fb3f5403863401793f38b37f8ac442f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Mar 2022 22:25:00 +0200 Subject: [PATCH] Refactoring + nouvelle table recap pour classiques (WIP) --- app/comp/inscr_mod.py | 2 +- app/comp/res_but.py | 277 +------- app/comp/res_classic.py | 17 +- app/comp/res_common.py | 704 ++++++++------------ app/pe/pe_jurype.py | 2 +- app/pe/pe_semestretag.py | 3 +- app/scodoc/sco_abs_views.py | 2 +- app/scodoc/sco_apogee_csv.py | 2 +- app/scodoc/sco_bulletins.py | 2 +- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_bulletins_xml.py | 2 +- app/scodoc/sco_debouche.py | 2 +- app/scodoc/sco_evaluations.py | 2 +- app/scodoc/sco_export_results.py | 2 +- app/scodoc/sco_formsemestre_exterieurs.py | 2 +- app/scodoc/sco_formsemestre_inscriptions.py | 2 +- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_formsemestre_validation.py | 2 +- app/scodoc/sco_groups.py | 2 +- app/scodoc/sco_liste_notes.py | 2 +- app/scodoc/sco_moduleimpl_inscriptions.py | 2 +- app/scodoc/sco_moduleimpl_status.py | 3 +- app/scodoc/sco_parcours_dut.py | 2 +- app/scodoc/sco_poursuite_dut.py | 2 +- app/scodoc/sco_prepajury.py | 2 +- app/scodoc/sco_pvjury.py | 2 +- app/scodoc/sco_recapcomplet.py | 22 +- app/scodoc/sco_report.py | 2 +- app/scodoc/sco_saisie_notes.py | 2 +- app/scodoc/sco_semset.py | 2 +- app/scodoc/sco_tag_module.py | 2 +- app/views/absences.py | 2 +- app/views/notes.py | 2 +- tests/unit/test_caches.py | 2 +- tests/unit/test_notes_modules.py | 2 +- tests/unit/test_sco_basic.py | 2 +- 36 files changed, 343 insertions(+), 743 deletions(-) diff --git a/app/comp/inscr_mod.py b/app/comp/inscr_mod.py index 667eacbd..cc03e673 100644 --- a/app/comp/inscr_mod.py +++ b/app/comp/inscr_mod.py @@ -17,7 +17,7 @@ from app import models def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame: """Charge la matrice des inscriptions aux modules du semestre rows: etudid (inscrits au semestre, avec DEM et DEF) - columns: moduleimpl_id (en chaîne) + columns: moduleimpl_id value: bool (0/1 inscrit ou pas) """ # méthode la moins lente: une requete par module, merge les dataframes diff --git a/app/comp/res_but.py b/app/comp/res_but.py index a81f01b5..bc6e7dd6 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -10,21 +10,15 @@ 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.res_compat 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_codes_parcours from app.scodoc.sco_codes_parcours import UE_SPORT -from app.scodoc import sco_groups from app.scodoc import sco_preferences -from app.scodoc import sco_users import app.scodoc.sco_utils as scu @@ -39,7 +33,6 @@ class ResultatsSemestreBUT(NotesTableCompat): 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)""" @@ -174,265 +167,11 @@ class ResultatsSemestreBUT(NotesTableCompat): ] 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 modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: + """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. + Utile pour stats bottom tableau recap. + Résultat: 1d array of float """ - - 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 - # les titres en footer: les mêmes, mais avec des bulles et liens: - titles_bot = {} - - 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["_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, - ) - titles_bot["_moy_gen_target_attrs"] = ( - 'title="moyenne indicative"' if self.is_apc else "" - ) - # --- 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, - ) - titles_bot[ - f"_{col_id}_target_attrs" - ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ - # 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}", - ) - titles_bot[f"_{col_id}_target"] = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - titles_bot[ - f"_{col_id}_target_attrs" - ] = f""" - title="{modimpl.module.titre} - ({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """ - 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) - titles_bot.update(titles) - footer_rows.append(titles_bot) - 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" + i = self.modimpl_coefs_df.columns.get_loc(modimpl_id) + j = self.modimpl_coefs_df.index.get_loc(ue_id) + return self.sem_cube[:, i, j] diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index b8f8ade0..605ba64c 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -16,7 +16,7 @@ from flask import g, url_for from app import db from app import log from app.comp import moy_mat, moy_mod, moy_sem, moy_ue, inscr_mod -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.etudiants import Identite @@ -40,6 +40,8 @@ class ResultatsSemestreClassic(NotesTableCompat): def __init__(self, formsemestre): super().__init__(formsemestre) + self.sem_matrix: np.ndarray = None + "sem_matrix : 2d-array (etuds x modimpls)" if not self.load_cached(): t0 = time.time() @@ -189,6 +191,19 @@ class ResultatsSemestreClassic(NotesTableCompat): ), } + def modimpl_notes( + self, + modimpl_id: int, + ue_id: int = None, + ) -> np.ndarray: + """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. + Utile pour stats bottom tableau recap. + ue_id n'est pas utilisé ici (formations classiques) + Résultat: 1d array of float + """ + i = self.modimpl_idx[modimpl_id] + return self.sem_matrix[:, i] + def compute_moyennes_matieres(self): """Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute.""" self.moyennes_matieres = moy_mat.compute_mat_moys_classic( diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 73734747..f89639e2 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,11 +9,8 @@ from functools import cached_property import numpy as np import pandas as pd -from flask import g, flash, url_for +from flask import g, url_for -from app import log -from app.comp.aux_stats import StatsMoyenne -from app.comp import moy_sem from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults @@ -21,10 +18,13 @@ from app.models import FormSemestre, FormSemestreUECoef from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns -from app.scodoc import sco_utils as scu from app.scodoc.sco_cache import ResultatsSemestreCache -from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_groups +from app.scodoc import sco_users +from app.scodoc import sco_utils as scu + # Il faut bien distinguer # - ce qui est caché de façon persistente (via redis): @@ -36,6 +36,10 @@ from app.scodoc.sco_exceptions import ScoValueError # qui sont notamment les attributs décorés par `@cached_property`` # class ResultatsSemestre(ResultatsCache): + """Les résultats (notes, ...) d'un formsemestre + Classe commune à toutes les formations (classiques, BUT) + """ + _cached_attrs = ( "etud_moy_gen_ranks", "etud_moy_gen", @@ -54,13 +58,18 @@ class ResultatsSemestre(ResultatsCache): # ResultatsSemestreBUT ou ResultatsSemestreClassic self.etud_moy_ue = {} "etud_moy_ue: DataFrame columns UE, rows etudid" - self.etud_moy_gen = {} + self.etud_moy_gen: pd.Series = None self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks_int = {} + self.modimpl_inscr_df: pd.DataFrame = None + "Inscriptions: row etudid, col modimlpl_id" self.modimpls_results: ModuleImplResults = None "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" self.etud_coef_ue_df = None """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" + self.modimpl_coefs_df: pd.DataFrame = None + """Coefs APC: rows = UEs (sans bonus), columns = modimpl, value = coef.""" + self.validations = None self.moyennes_matieres = {} """Moyennes de matières, si calculées. { matiere_id : Series, index etudid }""" @@ -151,17 +160,24 @@ class ResultatsSemestre(ResultatsCache): ues = sorted(list(ues), key=lambda x: x.numero or 0) return ues - def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]: + def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl de cette UE auxquels l'étudiant est inscrit. Utile en formations classiques, surchargée pour le BUT. + Inclus modules bonus le cas échéant. """ # sert pour l'affichage ou non de l'UE sur le bulletin - return [ + # Méthode surchargée en BUT + modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted if modimpl.module.ue.id == ue_id 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 @cached_property def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame: @@ -341,437 +357,275 @@ class ResultatsSemestre(ResultatsCache): # somme des coefs des modules de l'UE auxquels il est inscrit return self.compute_etud_ue_coef(etudid, ue) + # --- TABLEAU RECAP -# Pour raccorder le code des anciens codes qui attendent une NoteTable -class NotesTableCompat(ResultatsSemestre): - """Implementation partielle de NotesTable WIP TODO + def get_table_recap(self, convert_values=False): + """Result: tuple avec + - rows: liste de dicts { column_id : value } + - titles: { column_id : title } + - columns_ids: (liste des id de colonnes) - Les méthodes définies dans cette classe sont là - pour conserver la compatibilité abvec les codes anciens et - il n'est pas recommandé de les utiliser dans de nouveaux - développements (API malcommode et peu efficace). - """ + . 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 - _cached_attrs = ResultatsSemestre._cached_attrs + ( - "bonus", - "bonus_ues", - "malus", - "etud_moy_gen_ranks", - "etud_moy_gen_ranks_int", - "ue_rangs", - ) - - def __init__(self, formsemestre: FormSemestre): - super().__init__(formsemestre) - - nb_etuds = len(self.etuds) - self.bonus = None # virtuel - self.bonus_ues = None # virtuel - self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues} - self.mod_rangs = None # sera surchargé en Classic, mais pas en APC - """{ modimpl_id : (rangs, effectif) }""" - self.moy_min = "NA" - self.moy_max = "NA" - self.moy_moy = "NA" - self.expr_diagnostics = "" - self.parcours = self.formsemestre.formation.get_parcours() - - def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: - """Liste des étudiants inscrits - order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative) - - Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace - d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]` + 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 """ - etuds = self.formsemestre.get_inscrits( - include_demdef=include_demdef, order=(order_by == "nom") - ) - if order_by == "moy": - etuds.sort( - key=lambda e: ( - self.etud_moy_gen_ranks_int.get(e.id, 100000), - e.sort_key, - ) - ) - return etuds - def get_etudids(self) -> list[int]: - """(deprecated) - Liste des etudids inscrits, incluant les démissionnaires. - triée par ordre alphabetique de NOM - (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits) - """ - # Note: pour avoir les inscrits non triés, - # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ] - return [x["etudid"] for x in self.inscrlist] - - @cached_property - def sem(self) -> dict: - """le formsemestre, comme un gros et gras dict (nt.sem)""" - return self.formsemestre.get_infos_dict() - - @cached_property - def inscrlist(self) -> list[dict]: # utilisé par PE - """Liste des inscrits au semestre (avec DEM et DEF), - sous forme de dict etud, - classée dans l'ordre alphabétique de noms. - """ - etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True) - return [e.to_dict_scodoc7() for e in etuds] - - @cached_property - def stats_moy_gen(self): - """Stats (moy/min/max) sur la moyenne générale""" - return StatsMoyenne(self.etud_moy_gen) - - def get_ues_stat_dict( - self, filter_sport=False, check_apc_ects=True - ) -> list[dict]: # was get_ues() - """Liste des UEs, ordonnée par numero. - Si filter_sport, retire les UE de type SPORT. - Résultat: liste de dicts { champs UE U stats moyenne UE } - """ - ues = self.formsemestre.query_ues(with_sport=not filter_sport) - ues_dict = [] - for ue in ues: - d = ue.to_dict() - if ue.type != UE_SPORT: - moys = self.etud_moy_ue[ue.id] - else: - moys = None - d.update(StatsMoyenne(moys).to_dict()) - ues_dict.append(d) - if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"): - g.checked_apc_ects = True - if None in [ue.ects for ue in ues if ue.type != UE_SPORT]: - flash( - """Calcul moyenne générale impossible: ECTS des UE manquants !""", - category="danger", - ) - return ues_dict - - def get_modimpls_dict(self, ue_id=None) -> list[dict]: - """Liste des modules pour une UE (ou toutes si ue_id==None), - triés par numéros (selon le type de formation) - """ - modimpls_dict = [] - for modimpl in self.formsemestre.modimpls_sorted: - if ue_id == None or modimpl.module.ue.id == ue_id: - d = modimpl.to_dict() - # compat ScoDoc < 9.2: ajoute matières - d["mat"] = modimpl.module.matiere.to_dict() - modimpls_dict.append(d) - return modimpls_dict - - def compute_rangs(self): - """Calcule les classements - Moyenne générale: etud_moy_gen_ranks - Par UE (sauf ue bonus) - """ - ( - self.etud_moy_gen_ranks, - self.etud_moy_gen_ranks_int, - ) = moy_sem.comp_ranks_series(self.etud_moy_gen) - for ue in self.formsemestre.query_ues(): - moy_ue = self.etud_moy_ue[ue.id] - self.ue_rangs[ue.id] = ( - moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine - int(moy_ue.count()), - ) - # .count() -> nb of non NaN values - - def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]: - """Le rang de l'étudiant dans cette ue - Result: rang:str, effectif:str - """ - rangs, effectif = self.ue_rangs[ue_id] - if rangs is not None: - rang = rangs[etudid] + if convert_values: + fmt_note = scu.fmt_note else: - return "", "" - return rang, effectif + fmt_note = lambda x: x - def etud_check_conditions_ues(self, etudid): - """Vrai si les conditions sur les UE sont remplies. - Ne considère que les UE ayant des notes (moyenne calculée). - (les UE sans notes ne sont pas comptées comme sous la barre) - Prend en compte les éventuelles UE capitalisées. - - Pour les parcours habituels, cela revient à vérifier que - les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) - - Pour les parcours non standards (LP2014), cela peut être plus compliqué. - - Return: True|False, message explicatif - """ - ue_status_list = [] - for ue in self.formsemestre.query_ues(): - ue_status = self.get_etud_ue_status(etudid, ue.id) - if ue_status: - ue_status_list.append(ue_status) - return self.parcours.check_barre_ues(ue_status_list) - - def all_etuds_have_sem_decisions(self): - """True si tous les étudiants du semestre ont une décision de jury. - Ne regarde pas les décisions d'UE. - """ - for ins in self.formsemestre.inscriptions: - if ins.etat != scu.INSCRIT: - continue # skip démissionnaires - if self.get_etud_decision_sem(ins.etudid) is None: - return False - return True - - def etud_has_decision(self, etudid): - """True s'il y a une décision de jury pour cet étudiant""" - return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) - - def get_etud_decision_ues(self, etudid: int) -> dict: - """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. - Ne tient pas compte des UE capitalisées. - { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : } - Ne renvoie aucune decision d'UE pour les défaillants - """ - if self.get_etud_etat(etudid) == DEF: - return {} - else: - if not self.validations: - self.validations = res_sem.load_formsemestre_validations( - self.formsemestre - ) - return self.validations.decisions_jury_ues.get(etudid, None) - - def get_etud_decision_sem(self, etudid: int) -> dict: - """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. - { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } - Si état défaillant, force le code a DEF - """ - if self.get_etud_etat(etudid) == DEF: - return { - "code": DEF, - "assidu": False, - "event_date": "", - "compense_formsemestre_id": None, - } - else: - if not self.validations: - self.validations = res_sem.load_formsemestre_validations( - self.formsemestre - ) - return self.validations.decisions_jury.get(etudid, None) - - def get_etud_etat(self, etudid: int) -> str: - "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)" - ins = self.formsemestre.etuds_inscriptions.get(etudid, None) - if ins is None: - return "" - return ins.etat - - def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str: - """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" - if not self.moyennes_matieres: - return "nd" - return ( - self.moyennes_matieres[matiere_id].get(etudid, "-") - if matiere_id in self.moyennes_matieres - else "-" + 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 + # les titres en footer: les mêmes, mais avec des bulles et liens: + titles_bot = {} - def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: - """La moyenne de l'étudiant dans le moduleimpl - En APC, il s'agira d'une moyenne indicative sans valeur. - Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM) - """ - raise NotImplementedError() # virtual method + 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 - def get_etud_moy_gen(self, etudid): # -> float | str - """Moyenne générale de cet etudiant dans ce semestre. - Prend en compte les UE capitalisées. - Si apc, moyenne indicative. - Si pas de notes: 'NA' - """ - return self.etud_moy_gen[etudid] - - def get_etud_ects_pot(self, etudid: int) -> dict: - """ - Un dict avec les champs - ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury), - ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) - - Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non - encore enregistrées). - """ - # was nt.get_etud_moy_infos - # XXX pour compat nt, à remplacer ultérieurement - ues = self.get_etud_ue_validables(etudid) - ects_pot = 0.0 - for ue in ues: - if ( - ue.id in self.etud_moy_ue - and ue.ects is not None - and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE - ): - ects_pot += ue.ects - return { - "ects_pot": ects_pot, - "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) - } - - def get_etud_rang(self, etudid: int): - return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX - - def get_etud_rang_group(self, etudid: int, group_id: int): - return (None, 0) # XXX unimplemented TODO - - def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: - """Liste d'informations (compat NotesTable) sur évaluations completes - de ce module. - Évaluation "complete" ssi toutes notes saisies ou en attente. - """ - modimpl = ModuleImpl.query.get(moduleimpl_id) - modimpl_results = self.modimpls_results.get(moduleimpl_id) - if not modimpl_results: - return [] # safeguard - evals_results = [] - for e in modimpl.evaluations: - if modimpl_results.evaluations_completes_dict.get(e.id, False): - d = e.to_dict() - d["heure_debut"] = e.heure_debut # datetime.time - d["heure_fin"] = e.heure_fin - d["jour"] = e.jour # datetime - d["notes"] = { - etud.id: { - "etudid": etud.id, - "value": modimpl_results.evals_notes[e.id][etud.id], - } - for etud in self.etuds - } - d["etat"] = { - "evalattente": modimpl_results.evaluations_etat[e.id].nb_attente, - } - evals_results.append(d) - elif e.id not in modimpl_results.evaluations_completes_dict: - # ne devrait pas arriver ? XXX - log( - f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?" - ) - return evals_results - - def get_evaluations_etats(self): - """[ {...evaluation et son etat...} ]""" - # TODO: à moderniser - from app.scodoc import sco_evaluations - - if not hasattr(self, "_evaluations_etats"): - self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( - self.formsemestre.id - ) - - return self._evaluations_etats - - def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: - """Liste des états des évaluations de ce module""" - # XXX TODO à moderniser: lent, recharge des donénes que l'on a déjà... - return [ - e - for e in self.get_evaluations_etats() - if e["moduleimpl_id"] == moduleimpl_id - ] - - def get_moduleimpls_attente(self): - """Liste des modimpls du semestre ayant des notes en attente""" - return [ - modimpl - for modimpl in self.formsemestre.modimpls_sorted - if self.modimpls_results[modimpl.id].en_attente - ] - - def get_mod_stats(self, moduleimpl_id: int) -> dict: - """Stats sur les notes obtenues dans un modimpl - Vide en APC - """ - return { - "moy": "-", - "max": "-", - "min": "-", - "nb_notes": "-", - "nb_missing": "-", - "nb_valid_evals": "-", - } - - def get_nom_short(self, etudid): - "formatte nom d'un etud (pour table recap)" - etud = self.identdict[etudid] - return ( - (etud["nom_usuel"] or etud["nom"]).upper() - + " " - + etud["prenom"].capitalize()[:2] - + "." - ) - - @cached_property - def T(self): - return self.get_table_moyennes_triees() - - def get_table_moyennes_triees(self) -> list: - """Result: liste de tuples - moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid - """ - table_moyennes = [] 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["_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: - # pas de moyenne: démissionnaire ou def - t = ( - ["-"] - + ["0.00"] * len(self.ues) - + ["NI"] * len(self.formsemestre.modimpls_sorted) - ) - else: - moy_ues = [] - ue_is_cap = {} - for ue in ues: - ue_status = self.get_etud_ue_status(etudid, ue.id) - if ue_status: - moy_ues.append(ue_status["moy"]) - ue_is_cap[ue.id] = ue_status["is_capitalized"] - else: - moy_ues.append("?") - t = [moy_gen] + list(moy_ues) - # Moyennes modules: - for modimpl in self.formsemestre.modimpls_sorted: - if ue_is_cap.get(modimpl.module.ue.id, False): - val = "-c-" - else: - val = self.get_etud_mod_moy(modimpl.id, etudid) - t.append(val) - t.append(etudid) - table_moyennes.append(t) - # tri par moyennes décroissantes, - # en laissant les démissionnaires à la fin, par ordre alphabetique - etuds = [ins.etud for ins in etuds_inscriptions.values()] - etuds.sort(key=lambda e: e.sort_key) - self._rang_alpha = {e.id: i for i, e in enumerate(etuds)} - table_moyennes.sort(key=self._row_key) - return table_moyennes + 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, + ) + titles_bot["_moy_gen_target_attrs"] = ( + 'title="moyenne indicative"' if self.is_apc else "" + ) + # --- 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, + ) + titles_bot[ + f"_{col_id}_target_attrs" + ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ + # Les moyennes des modules (ou 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 + if self.is_apc: # BUT + 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: # classique: Series indépendante de l'UE + val = modimpl_results.etuds_moy_module.get( + etudid, "?" + ) + else: + val = "" - def _row_key(self, x): - """clé de tri par moyennes décroissantes, - en laissant les demissionnaires à la fin, par ordre alphabetique. - (moy_gen, rang_alpha) - """ - try: - moy = -float(x[0]) - except (ValueError, TypeError): - moy = 1000.0 - return (moy, self._rang_alpha[x[-1]]) + 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}", + ) + titles_bot[f"_{col_id}_target"] = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ) + titles_bot[ + f"_{col_id}_target_attrs" + ] = f""" + title="{modimpl.module.titre} + ({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """ + modimpl_ids.add(modimpl.id) - @cached_property - def identdict(self) -> dict: - """{ etudid : etud_dict } pour tous les inscrits au semestre""" - return { - ins.etud.id: ins.etud.to_dict_scodoc7() - for ins in self.formsemestre.inscriptions + 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) + titles_bot.update(titles) + footer_rows.append(titles_bot) + 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}" + if self.is_apc: + coef = self.modimpl_coefs_df[modimpl.id][ue.id] * ( + modimpl.module.coefficient or 0.0 + ) + else: + coef = modimpl.module.coefficient or 0 + bottom_infos["coef"][col_id] = fmt_note(coef) + notes = self.modimpl_notes(modimpl.id, ue.id) + 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): + """Table recap: ajoute à row les colonnes sur les groupes pour cet etud""" + # dec = self.get_etud_decision_sem(etudid) + # if dec: + # codes_nb[dec["code"]] += 1 + row_class = "" + etud_etat = self.get_etud_etat(etudid) # dans NotesTableCompat, à revoir + if etud_etat == DEM: + gr_name = "Dém." + row_class = "dem" + elif etud_etat == 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" diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 2720ad43..0943649c 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -47,7 +47,7 @@ import os from zipfile import ZipFile from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc.gen_tables import GenTable, SeqGenTable diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index 43ea48d7..0a7adcab 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -38,11 +38,10 @@ Created on Fri Sep 9 09:15:05 2016 from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.moduleimpls import ModuleImpl -from app.models.ues import UniteEns from app.scodoc import sco_codes_parcours from app.scodoc import sco_tag_module from app.pe import pe_tagtable diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index 1c4ff8fe..a4e1d3a7 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -34,7 +34,7 @@ from flask import url_for, g, request, abort from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import Identite, FormSemestre import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index b1482ec5..3f979677 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -97,7 +97,7 @@ from chardet import detect as chardet_detect from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, Identite from app.models.config import ScoDocSiteConfig import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index c4b10c01..bb1d933d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -39,7 +39,7 @@ from app import email from app import log from app.but import bulletin_but from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, Identite, ModuleImplInscription from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import AccessDenied, ScoValueError diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index ee0ddae2..f34923a5 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -33,7 +33,7 @@ import json from app.but import bulletin_but from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models.formsemestre import FormSemestre from app.models.etudiants import Identite diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index f41361c2..d6925d8c 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -45,7 +45,7 @@ from xml.etree import ElementTree from xml.etree.ElementTree import Element from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log diff --git a/app/scodoc/sco_debouche.py b/app/scodoc/sco_debouche.py index a567a344..ea96ff1c 100644 --- a/app/scodoc/sco_debouche.py +++ b/app/scodoc/sco_debouche.py @@ -33,7 +33,7 @@ from flask import url_for, g, request from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index d895a8a3..43b58590 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -39,7 +39,7 @@ from flask import request from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 1003790b..30750e3e 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -30,7 +30,7 @@ from flask import url_for, g, request from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 99f64a22..9dfcbbb6 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -38,7 +38,7 @@ from flask import url_for, g, request from flask_login import current_user from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 44c75dd0..b3fe9853 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -33,7 +33,7 @@ import flask from flask import url_for, g, request from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu from app import log diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index ae9ce543..275605d8 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -36,7 +36,7 @@ from flask_login import current_user from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import Module from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 43f10ad8..96d0a737 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -37,7 +37,7 @@ import app.scodoc.sco_utils as scu from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.notes import etud_has_notes_attente diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 2d0aec60..8e834b23 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -46,7 +46,7 @@ from flask import url_for, make_response from app import db from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, formsemestre from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models.groups import Partition diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index e12b7a54..7a22ccbe 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -38,7 +38,7 @@ from app import models from app.comp import res_sem from app.comp import moy_mod from app.comp.moy_mod import ModuleImplResults -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre from app.models.evaluations import Evaluation diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index 8e2e8b2b..76871cf3 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -34,7 +34,7 @@ from flask import url_for, g, request from flask_login import current_user from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.notesdb as ndb diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 52d3282f..a82cc2b1 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -34,8 +34,7 @@ from flask_login import current_user from app.auth.models import User from app.comp import res_sem -from app.comp.res_common import NotesTableCompat -from app.models import FormSemestre +from app.comp.res_compat import NotesTableCompat from app.models import ModuleImpl from app.models.evaluations import Evaluation import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index 55110e5b..8c1d23a4 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -29,7 +29,7 @@ """ from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, UniteEns import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index cfa3dff8..41b29259 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -34,7 +34,7 @@ import collections from flask import url_for, g, request from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc import sco_abs diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index df0cb4af..678b81ab 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -36,7 +36,7 @@ from flask import request from flask_login import current_user from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, Identite from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 00e88751..cfa7bae6 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -55,7 +55,7 @@ import flask from flask import url_for, g, request from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre, UniteEns import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 31b86297..36d92e76 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -38,15 +38,13 @@ from flask import make_response, url_for from app import log from app.but import bulletin_but from app.comp import res_sem -from app.comp.res_but import ResultatsSemestreBUT -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite from app.models.evaluations import Evaluation import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header -from app.scodoc import sco_bac from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_xml from app.scodoc import sco_bulletins, sco_excel @@ -225,12 +223,9 @@ def do_formsemestre_recapcomplet( ): """Calcule et renvoie le tableau récapitulatif.""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - if ( - formsemestre.formation.is_apc() - and format not in ("xml", "json") - and not modejury - ): - data, filename = make_formsemestre_recapcomplet_apc(formsemestre, format=format) + if format == "html" and not modejury: + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + data, filename = gen_formsemestre_recapcomplet_html(formsemestre, res) else: data, filename, format = make_formsemestre_recapcomplet( formsemestre_id=formsemestre_id, @@ -1037,14 +1032,13 @@ def _gen_row(keys: list[str], row, elt="td"): return f'{"".join([_gen_cell(key, row, elt) for key in keys])}' -def make_formsemestre_recapcomplet_apc(formsemestre: FormSemestre, format="html"): +def gen_formsemestre_recapcomplet_html( + formsemestre: FormSemestre, res: NotesTableCompat +): """Construit table recap pour le BUT Return: data, filename """ - res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) - rows, footer_rows, titles, column_ids = res.get_table_moyennes_triees( - convert_values=True - ) + rows, footer_rows, titles, column_ids = res.get_table_recap(convert_values=True) H = ['
'] # header H.append( diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index fff02647..214532a5 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -40,7 +40,7 @@ from flask import url_for, g, request import pydot from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d982b5e7..fbef9245 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -37,7 +37,7 @@ from flask import g, url_for, request from flask_login import current_user from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 10c06c9e..37b39fa8 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -43,7 +43,7 @@ import flask from flask import g from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc import html_sco_header from app.scodoc import sco_cache diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 45ed6e07..1b41cd87 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -38,7 +38,7 @@ import http from flask import g, url_for from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb diff --git a/app/views/absences.py b/app/views/absences.py index f7523284..809395d2 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -62,7 +62,7 @@ from flask_login import current_user from app import log from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.decorators import ( scodoc, scodoc7func, diff --git a/app/views/notes.py b/app/views/notes.py index fd39f271..35f92e0b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -42,7 +42,7 @@ from flask_login import current_user from werkzeug.utils import redirect from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.ues import UniteEns diff --git a/tests/unit/test_caches.py b/tests/unit/test_caches.py index 27694bb4..fc2cb770 100644 --- a/tests/unit/test_caches.py +++ b/tests/unit/test_caches.py @@ -14,7 +14,7 @@ from flask import current_app, g import app from app import db from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc import sco_cache from app.scodoc import sco_evaluations diff --git a/tests/unit/test_notes_modules.py b/tests/unit/test_notes_modules.py index adb118c6..f4e23e31 100644 --- a/tests/unit/test_notes_modules.py +++ b/tests/unit/test_notes_modules.py @@ -11,7 +11,7 @@ from flask import g import app from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc import sco_bulletins, sco_formsemestre from app.scodoc import sco_cache diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index b822ecee..a9f6b19a 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -20,7 +20,7 @@ from tests.unit import sco_fake_gen import app from app.comp import res_sem -from app.comp.res_common import NotesTableCompat +from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc import sco_formsemestre from app.scodoc import sco_abs