From 574f7fc376d393ce3b94f1e18f859795fb056bed Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 3 Feb 2023 15:58:52 +0100 Subject: [PATCH] WIP: refactoring --- app/but/jury_but_recap.py | 565 ++++++++++++++++++++++++++++++++++++++ app/comp/res_common.py | 490 +++++++++++++++++---------------- sco_version.py | 2 +- 3 files changed, 824 insertions(+), 233 deletions(-) create mode 100644 app/but/jury_but_recap.py diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py new file mode 100644 index 00000000..2e56ca3c --- /dev/null +++ b/app/but/jury_but_recap.py @@ -0,0 +1,565 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: table recap annuelle et liens saisie +""" + +import collections +import time +import numpy as np +from flask import g, url_for + +from app.but import jury_but +from app.but.jury_but import ( + DecisionsProposeesAnnee, + DecisionsProposeesRCUE, + DecisionsProposeesUE, +) +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import res_sem +from app.models.etudiants import Identite +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences +from app.models.formsemestre import FormSemestre +from app.scodoc import html_sco_header +from app.scodoc.sco_codes_parcours import ( + BUT_BARRE_RCUE, + BUT_BARRE_UE, + BUT_BARRE_UE8, + BUT_RCUE_SUFFISANT, +) +from app.scodoc import sco_formsemestre_status +from app.scodoc import sco_pvjury +from app.scodoc import sco_utils as scu +from app.scodoc import table_builder as tb + + +class TableJury(tb.Table): + pass + + +class RowJury(tb.Row): + "Ligne de la table saisie jury" + + def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): + "cell avec nb niveaux validables / total" + classes = ["col_rcue", "col_rcues_validables"] + if deca.nb_rcues_under_8 > 0: + classes.append("moy_ue_warning") + elif deca.nb_validables < deca.nb_competences: + classes.append("moy_ue_inf") + else: + classes.append("moy_ue_valid") + + if len(deca.rcues_annee) > 0: + # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair + if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: + moy = deca.res_pair.etud_moy_gen[deca.etud.id] + if np.isnan(moy): + moy_gen_d = "x" + else: + moy_gen_d = f"{int(moy*1000):05}" + else: + moy_gen_d = "x" + order = f"{deca.nb_validables:04d}-{moy_gen_d}" + else: + # étudiants sans RCUE: pas de semestre impair, ... + # les classe à la fin + order = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}" + + self.add_cell( + "rcues_validables", + "RCUEs", + f"""{deca.nb_validables}/{deca.nb_competences}""" + + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + group="rcues_validables", + classes=classes, + data={"order": order}, + ) + + def add_ue_cells(self, dec_ue: DecisionsProposeesUE): + "cell de moyenne d'UE" + col_id = f"moy_ue_{dec_ue.ue.id}" + note_class = "" + val = dec_ue.moy_ue + if isinstance(val, float): + if val < BUT_BARRE_UE: + note_class = "moy_inf" + elif val >= BUT_BARRE_UE: + note_class = "moy_ue_valid" + if val < BUT_BARRE_UE8: + note_class = "moy_ue_warning" # notes très basses + self.add_cell( + col_id, + dec_ue.ue.acronyme, + self.fmt_note(val), + group="col_ue", + "col_ue" + note_class, + column_class="col_ue", + ) + self.add_cell( + col_id + "_code", + dec_ue.ue.acronyme, + dec_ue.code_valide or "", + "col_ue_code recorded_code", + column_class="col_ue", + ) + + +def formsemestre_saisie_jury_but( + formsemestre2: FormSemestre, + read_only: bool = False, + selected_etudid: int = None, + mode="jury", +) -> str: + """formsemestre est un semestre PAIR + Si readonly, ne montre pas le lien "saisir la décision" + + => page html complète + + Si mode == "recap", table recap des codes, sans liens de saisie. + """ + # Quick & Dirty + # pour chaque etud de res2 trié + # S1: UE1, ..., UEn + # S2: UE1, ..., UEn + # + # UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue + # + # Pour chaque etud de res2 trié + # DecisionsProposeesAnnee(etud, formsemestre2) + # Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur + # -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc + + if formsemestre2.formation.referentiel_competence is None: + raise ScoNoReferentielCompetences(formation=formsemestre2.formation) + + rows, titles, column_ids, jury_stats = get_jury_but_table( + formsemestre2, read_only=read_only, mode=mode + ) + if not rows: + return ( + '
aucun étudiant !
' + ) + filename = scu.sanitize_filename( + f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + klass = "table_jury_but_bilan" if mode == "recap" else "" + table_html = build_table_jury_but_html( + filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass + ) + H = [ + html_sco_header.sco_header( + page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre2.id + ), + ] + if mode == "recap": + H.append( + f"""

Décisions de jury enregistrées pour les étudiants de ce semestre

+ + """ + ) + H.append( + f""" + + {table_html} + + + +
+
Nb d'étudiants avec décision annuelle: + {sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]} +
+
Codes annuels octroyés:
+ + """ + ) + for code in sorted(jury_stats["codes_annuels"].keys()): + H.append( + f""" + + + + """ + ) + H.append( + f""" +
{code}{jury_stats["codes_annuels"][code]}{ + (100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}% +
+
+ {html_sco_header.sco_footer()} + """ + ) + return "\n".join(H) + + +def build_table_jury_but_html( + filename: str, rows, titles, column_ids, selected_etudid: int = None, klass="" +) -> str: + """assemble la table html""" + footer_rows = [] # inutilisé pour l'instant + H = [ + f"""
""" + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th")} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append("\n") + # footer + H.append("") + idx_last = len(footer_rows) - 1 + for i, row in enumerate(footer_rows): + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append( + """ + +
+
+ """ + ) + return "".join(H) + + +class RowCollector: + """Une ligne de la table""" + + def __init__( + self, + cells: dict = None, + titles: dict = None, + convert_values=True, + column_classes: dict = None, + ): + self.titles = titles + self.row = cells or {} # col_id : str + self.column_classes = column_classes # col_id : str, css class + self.idx = 0 + self.last_etud_cell_idx = 0 + if convert_values: + self.fmt_note = scu.fmt_note + else: + self.fmt_note = lambda x: x + + def __setitem__(self, key, value): + self.row[key] = value + + def __getitem__(self, key): + return self.row[key] + + def get_row_dict(self): + "La ligne, comme un dict" + # create empty cells + for col_id in self.titles: + if col_id not in self.row: + self.row[col_id] = "" + klass = self.column_classes.get(col_id) + if klass: + self.row[f"_{col_id}_class"] = klass + return self.row + + def add_cell( + self, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = None, + column_class="", + ): + """Add a row to our table. classes is a list of css class names""" + self.idx = idx if idx is not None else self.idx + self.row[col_id] = content + if classes: + self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" + if not col_id in self.titles: + self.titles[col_id] = title + self.titles[f"_{col_id}_col_order"] = self.idx + if classes: + self.titles[f"_{col_id}_class"] = classes + self.column_classes[col_id] = column_class + self.idx += 1 + + # def add_etud_cells( + # self, etud: Identite, formsemestre: FormSemestre, with_links=True + # ): + # "Les cells code, nom, prénom etc." + # # --- Codes (seront cachés, mais exportés en excel) + # self.add_cell("etudid", "etudid", etud.id, "codes") + # self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") + # # --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO) + # self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + # self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") + # self["_nom_disp_order"] = etud.sort_key + # self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") + # self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + # self["_nom_short_data"] = { + # "etudid": etud.id, + # "nomprenom": etud.nomprenom, + # } + # if with_links: + # self["_nom_short_order"] = etud.sort_key + # self["_nom_short_target"] = url_for( + # "notes.formsemestre_bulletinetud", + # scodoc_dept=g.scodoc_dept, + # formsemestre_id=formsemestre.id, + # etudid=etud.id, + # ) + # self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"' + # self["_nom_disp_target"] = self["_nom_short_target"] + # self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] + # self.last_etud_cell_idx = self.idx + + + + def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE): + "2 cells: moyenne du RCUE, code enregistré" + rcue = dec_rcue.rcue + col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id + note_class = "" + val = rcue.moy_rcue + if isinstance(val, float): + if val < BUT_BARRE_RCUE: + note_class = " moy_ue_inf" + elif val >= BUT_BARRE_RCUE: + note_class = " moy_ue_valid" + if val < BUT_RCUE_SUFFISANT: + note_class = " moy_ue_warning" # notes très basses + self.add_cell( + col_id, + f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", + self.fmt_note(val), + "col_rcue" + note_class, + column_class="col_rcue", + ) + self.add_cell( + col_id + "_code", + f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", + dec_rcue.code_valide or "", + "col_rcue_code recorded_code", + column_class="col_rcue", + ) + + +def get_jury_but_table( + formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True +) -> tuple[list[dict], list[str], list[str], dict]: + """Construit la table des résultats annuels pour le jury BUT + => rows_dict, titles, column_ids, jury_stats + où jury_stats est un dict donnant des comptages sur le jury. + """ + res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) + titles = {} # column_id : title + jury_stats = { + "nb_etuds": len(formsemestre2.etuds_inscriptions), + "codes_annuels": collections.Counter(), + } + table = TableJury(res2, mode_jury=True) + for etudid in formsemestre2.etuds_inscriptions: + etud: Identite = Identite.query.get(etudid) + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) + # XXX row = RowCollector(titles=titles, column_classes=column_classes) + row = RowJury(table, etudid) + table.add_row(row) + row.add_etud(etud) + # --- Nombre de niveaux + row.add_nb_rcues_cell(deca) + # --- Les RCUEs + for rcue in deca.rcues_annee: + dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) + if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau + row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) + row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) + row.add_rcue_cells(dec_rcue) + # --- Les ECTS validés + ects_valides = 0.0 + if deca.res_impair: + ects_valides += deca.res_impair.get_etud_ects_valides(etudid) + if deca.res_pair: + ects_valides += deca.res_pair.get_etud_ects_valides(etudid) + row.add_cell( + "ects_annee", + "ECTS", + f"""{int(ects_valides)}""", + "col_code_annee", + ) + # --- Le code annuel existant + row.add_cell( + "code_annee", + "Année", + f"""{deca.code_valide or ''}""", + "col_code_annee", + ) + if deca.code_valide: + jury_stats["codes_annuels"][deca.code_valide] += 1 + # --- Le lien de saisie + if mode != "recap" and with_links: + row.add_cell( + "lien_saisie", + "", + f""" + + {"voir" if read_only else ("modif." if deca.code_valide else "saisie")} + décision + """ + if deca.inscription_etat == scu.INSCRIT + else deca.inscription_etat, + "col_lien_saisie_but", + ) + rows.append(row) + rows_dict = [row.get_row_dict() for row in rows] + if len(rows_dict) > 0: + col_idx = res2.recap_add_partitions( + rows_dict, titles, col_idx=row.last_etud_cell_idx + 1 + ) + res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1) + column_ids = [title for title in titles if not title.startswith("_")] + column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) + rows_dict.sort(key=lambda row: row["_nom_disp_order"]) + return rows_dict, titles, column_ids, jury_stats + + +def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: + """Liste des résultats jury BUT sous forme de dict, pour API""" + if formsemestre.formation.referentiel_competence is None: + # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) + return [] + dpv = sco_pvjury.dict_pvjury(formsemestre.id) + rows = [] + for etudid in formsemestre.etuds_inscriptions: + rows.append(get_jury_but_etud_result(formsemestre, dpv, etudid)) + return rows + + +def get_jury_but_etud_result( + formsemestre: FormSemestre, dpv: dict, etudid: int +) -> dict: + """Résultats de jury d'un étudiant sur un semestre pair de BUT""" + etud: Identite = Identite.query.get(etudid) + dec_etud = dpv["decisions_dict"][etudid] + if formsemestre.formation.is_apc(): + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + else: + deca = None + row = { + "etudid": etud.id, + "code_nip": etud.code_nip, + "code_ine": etud.code_ine, + "is_apc": dpv["is_apc"], # BUT ou classic ? + "etat": dec_etud["etat"], # I ou D ou DEF + "nb_competences": deca.nb_competences if deca else 0, + } + # --- Les RCUEs + rcue_list = [] + if deca: + for rcue in deca.rcues_annee: + dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) + if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau + dec_ue1 = deca.decisions_ues[rcue.ue_1.id] + dec_ue2 = deca.decisions_ues[rcue.ue_2.id] + rcue_dict = { + "ue_1": { + "ue_id": rcue.ue_1.id, + "moy": None + if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue)) + else dec_ue1.moy_ue, + "code": dec_ue1.code_valide, + }, + "ue_2": { + "ue_id": rcue.ue_2.id, + "moy": None + if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue)) + else dec_ue2.moy_ue, + "code": dec_ue2.code_valide, + }, + "moy": rcue.moy_rcue, + "code": dec_rcue.code_valide, + } + rcue_list.append(rcue_dict) + row["rcues"] = rcue_list + # --- Les UEs + ue_list = [] + if dec_etud["decisions_ue"]: + for ue_id, ue_dec in dec_etud["decisions_ue"].items(): + ue_dict = { + "ue_id": ue_id, + "code": ue_dec["code"], + "ects": ue_dec["ects"], + } + ue_list.append(ue_dict) + row["ues"] = ue_list + # --- Le semestre (pour les formations classiques) + if dec_etud["decision_sem"]: + row["semestre"] = {"code": dec_etud["decision_sem"].get("code")} + else: + row["semestre"] = {} # APC, ... + # --- Autorisations + row["autorisations"] = dec_etud["autorisations"] + return row diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 8a4da4de..36ae60d8 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -454,112 +454,238 @@ 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 - def get_table_recap( +# --- TABLEAU RECAP + + +class TableRecap(tb.Table): # was get_table_recap + """Table récap. des résultats. + allow_html: si vrai, peut mettre du HTML dans les valeurs + + Result: Table, le contenu étant une ligne par étudiant. + + + 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 classes: + - pour les lignes: + selected_row pour l'étudiant sélectionné + - les colonnes: + - 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_ + """ + + def __init__( self, + res: ResultatsSemestre, convert_values=False, include_evaluations=False, mode_jury=False, - ) -> tb.Table: - """Table récap. des résultats. - allow_html: si vrai, peut mettre du HTML dans les valeurs + ): + self.res = res + self.include_evaluations = include_evaluations + self.mode_jury = mode_jury - Result: Table, le contenu étant une ligne par étudiant. - - - 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 classes: - - pour les lignes: - selected_row pour l'étudiant sélectionné - - les colonnes: - - 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_ - """ + parcours = self.formsemestre.formation.get_parcours() + self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE + self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE + self.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING + self.cache_nomcomplet = {} # cache uid : nomcomplet + if convert_values: + self.fmt_note = scu.fmt_note + else: + self.fmt_note = lambda x: x + # couples (modimpl, ue) effectivement présents dans la table: + self.modimpl_ue_ids = set() etuds_inscriptions = self.formsemestre.etuds_inscriptions ues = self.formsemestre.query_ues(with_sport=True) # avec bonus ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT] - table = tb.Table() - # Quelques infos stockées dans la Table - parcours = self.formsemestre.formation.get_parcours() - table.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE - table.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE - table.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING - table.cache_nomcomplet = {} # cache uid : nomcomplet - if convert_values: - table.fmt_note = scu.fmt_note - else: - table.fmt_note = lambda x: x - # couples (modimpl, ue) effectivement présents dans la table: - table.modimpl_ue_ids = set() - for etudid in etuds_inscriptions: etud = Identite.query.get(etudid) - row = tb.Row(table, etudid) - table.add_row(row) - self._recap_add_etud(row, etud) - self._recap_add_moyennes(row, etud, ues_sans_bonus, mode_jury) + row = RowRecap(self, etudid) + self.add_row(row) + self.recap_add_etud(row, etud) + self._recap_add_moyennes(row, etud, ues_sans_bonus) - self.recap_add_partitions(table) - self.recap_add_cursus(table) - self._recap_add_admissions(table) + self.recap_add_partitions() + self.recap_add_cursus() + self._recap_add_admissions() # tri par rang croissant if not self.formsemestre.block_moyenne_generale: - table.sort_rows(key=lambda row: row.rang_order) + self.sort_rows(key=lambda row: row.rang_order) else: - table.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True) + self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True) # Lignes footer (min, max, ects, apo, ...) - self._recap_add_bottom_rows(table, ues_sans_bonus) + self.add_bottom_rows(ues_sans_bonus) # Evaluations: if include_evaluations: - self._recap_add_evaluations(table) + self.add_evaluations() - # Ajoute style "col_empty" aux colonnes de modules vides - row_moy = table.get_row_by_id("moy") - for col_id in table.column_ids: + self.mark_empty_cols() + self.add_type_row() + + def mark_empty_cols(self): + """Ajoute style "col_empty" aux colonnes de modules vides""" + # identifie les col. vides par la classe sur leur moyenne + row_moy = self.get_row_by_id("moy") + for col_id in self.column_ids: cell: tb.Cell = row_moy.cells.get(col_id) if cell and "col_empty" in cell.classes: - table.column_classes[col_id].append("col_empty") + self.column_classes[col_id].append("col_empty") - # Ligne avec la classe de chaque colonne + def add_type_row(self): + """Ligne avec la classe de chaque colonne recap.""" # récupère le type à partir des classes css (hack...) row_type = tb.BottomRow( - table, + self, "type_col", left_title="Type col.", left_title_col_ids=["prenom", "nom_short"], category="bottom_infos", classes=["bottom_info"], ) - for col_id in table.column_ids: - group_name = table.column_group.get(col_id, "") + for col_id in self.column_ids: + group_name = self.column_group.get(col_id, "") if group_name.startswith("col_"): group_name = group_name[4:] row_type.add_cell(col_id, None, group_name) - return table + def add_bottom_rows(self, ues): + """Les informations à mettre en bas de la table recap: + min, max, moy, ECTS, Apo.""" + res = self.res + # Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo + row_min = tb.BottomRow( + self, + "min", + left_title="Min.", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) + row_max = tb.BottomRow( + self, + "max", + left_title="Max.", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) + row_moy = tb.BottomRow( + self, + "moy", + left_title="Moy.", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) + row_coef = tb.BottomRow( + self, + "coef", + left_title="Coef.", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) + row_ects = tb.BottomRow( + self, + "ects", + left_title="ECTS", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) + row_apo = tb.BottomRow( + self, + "apo", + left_title="Code Apogée", + left_title_col_ids=["prenom", "nom_short"], + category="bottom_infos", + classes=["bottom_info"], + ) - def _recap_add_etud(self, row: tb.Row, etud: Identite): + # --- ECTS + # titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms + for ue in ues: + col_id = f"moy_ue_{ue.id}" + row_ects.add_cell(col_id, None, ue.ects) + # ajoute cell UE vides sur ligne coef pour borders verticales + # XXX TODO classes dans table sur colonne ajoutées à tous les TD + row_coef.add_cell(col_id, None, "") + row_ects.add_cell( + "moy_gen", + None, + sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]), + ) + # --- MIN, MAX, MOY, APO + row_min.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.min())) + row_max.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.max())) + row_moy.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.mean())) + + for ue in ues: + col_id = f"moy_ue_{ue.id}" + row_min.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].min())) + row_max.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].max())) + row_moy.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].mean())) + row_apo.add_cell(col_id, None, ue.code_apogee or "") + + for modimpl in res.formsemestre.modimpls_sorted: + if (modimpl.id, ue.id) in self.modimpl_ue_ids: + col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" + if res.is_apc: + coef = res.modimpl_coefs_df[modimpl.id][ue.id] + else: + coef = modimpl.module.coefficient or 0 + row_coef.add_cell( + col_id, + None, + self.fmt_note(coef), + group=f"col_ue_{ue.id}_modules", + ) + notes = res.modimpl_notes(modimpl.id, ue.id) + if np.isnan(notes).all(): + # aucune note valide + row_min.add_cell(col_id, None, np.nan) + row_max.add_cell(col_id, None, np.nan) + moy = np.nan + else: + row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes))) + row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes))) + moy = np.nanmean(notes) + row_moy.add_cell( + col_id, + None, + self.fmt_note(moy), + # aucune note dans ce module ? + classes=["col_empty" if np.isnan(moy) else ""], + ) + row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "") + + +class RowRecap(tb.Row): + "Ligne de la table recap" + + def add_etud(self, etud: Identite): """Ajoute colonnes étudiant: codes, noms""" + res = self.table.res # --- Codes (seront cachés, mais exportés en excel) - row.add_cell("etudid", "etudid", etud.id, "etud_codes") - row.add_cell( + self.add_cell("etudid", "etudid", etud.id, "etud_codes") + self.add_cell( "code_nip", "code_nip", etud.code_nip or "", @@ -567,26 +693,26 @@ class ResultatsSemestre(ResultatsCache): ) # --- Rang - if not self.formsemestre.block_moyenne_generale: - row.rang_order = self.etud_moy_gen_ranks_int[etud.id] - row.add_cell( + if not res.formsemestre.block_moyenne_generale: + self.rang_order = res.etud_moy_gen_ranks_int[etud.id] + res.add_cell( "rang", "Rg", self.etud_moy_gen_ranks[etud.id], "rang", - data={"order": f"{row.rang_order:05d}"}, + data={"order": f"{self.rang_order:05d}"}, ) else: - row.rang_order = -1 + self.rang_order = -1 # --- Identité étudiant url_bulletin = url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, - formsemestre_id=self.formsemestre.id, + formsemestre_id=res.formsemestre.id, etudid=etud.id, ) - row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") - row.add_cell( + self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + self.add_cell( "nom_disp", "Nom", etud.nom_disp(), @@ -595,8 +721,8 @@ class ResultatsSemestre(ResultatsCache): target=url_bulletin, target_attrs={"class": "etudinfo", "id": str(etud.id)}, ) - row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") - row.add_cell( + self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") + self.add_cell( "nom_short", "Nom", etud.nom_short, @@ -610,19 +736,18 @@ class ResultatsSemestre(ResultatsCache): target_attrs={"class": "etudinfo", "id": str(etud.id)}, ) - def _recap_add_moyennes( + def add_moyennes( # XXX was _recap_add_moyennes self, row: tb.Row, etud: Identite, ues_sans_bonus: list[UniteEns], - mode_jury=False, ): """Ajoute cols moy_gen moy_ue et tous les modules...""" - table = row.table - + table = self.table + res = table.res # --- Moyenne générale - if not self.formsemestre.block_moyenne_generale: - moy_gen = self.etud_moy_gen.get(etud.id, False) + if not res.formsemestre.block_moyenne_generale: + moy_gen = res.etud_moy_gen.get(etud.id, False) note_class = "" if moy_gen is False: moy_gen = scu.NO_NOTE_STR @@ -636,25 +761,25 @@ class ResultatsSemestre(ResultatsCache): classes=[note_class], ) # Ajoute bulle sur titre du pied de table: - if self.is_apc: + if res.is_apc: table.foot_title_row.cells["moy_gen"].target_attrs[ "title" ] = "moyenne indicative" # --- Moyenne d'UE - row.nb_ues_validables, row.nb_ues_warning = 0, 0 + self.nb_ues_validables, self.nb_ues_warning = 0, 0 for ue in ues_sans_bonus: - ue_status = self.get_etud_ue_status(etud.id, ue.id) + ue_status = res.get_etud_ue_status(etud.id, ue.id) if ue_status is not None: - self._recap_add_ue(row, ue, ue_status) - if mode_jury: + self.add_ue(ue, ue_status) + if table.mode_jury: # pas d'autre colonnes de résultats continue # Bonus (sport) dans cette UE ? # Le bonus sport appliqué sur cette UE - if (self.bonus_ues is not None) and (ue.id in self.bonus_ues): - val = self.bonus_ues[ue.id][etud.id] or "" + if (res.bonus_ues is not None) and (ue.id in res.bonus_ues): + val = res.bonus_ues[ue.id][etud.id] or "" val_fmt = val_fmt_html = table.fmt_note(val) if val: val_fmt_html = f"""{ @@ -669,20 +794,22 @@ class ResultatsSemestre(ResultatsCache): classes=["col_ue_bonus"], ) # Les moyennes des modules (ou ressources et SAÉs) dans cette UE - self._recap_add_ue_modimpls(row, ue, etud, ue_status["is_capitalized"]) + self.add_ue_modimpls( + ue, etud, ue_status["is_capitalized"] + ) # XXX _recap_add_ue_modimpls - row.nb_ues_etud_parcours = len(self.etud_ues_ids(etud.id)) + self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id)) ue_valid_txt = ( ue_valid_txt_html - ) = f"{row.nb_ues_validables}/{row.nb_ues_etud_parcours}" - if row.nb_ues_warning: + ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" + if self.nb_ues_warning: ue_valid_txt_html += " " + scu.EMO_WARNING # place juste avant moy. gen. table.insert_group("col_ues_validables", before="col_moy_gen") classes = ["col_ue"] - if row.nb_ues_warning: + if self.nb_ues_warning: classes.append("moy_ue_warning") - elif row.nb_ues_validables < len(ues_sans_bonus): + elif self.nb_ues_validables < len(ues_sans_bonus): classes.append("moy_inf") row.add_cell( "ues_validables", @@ -691,13 +818,13 @@ class ResultatsSemestre(ResultatsCache): "col_ues_validables", classes=classes, raw_content=ue_valid_txt, - data={"order": row.nb_ues_validables}, # tri + data={"order": self.nb_ues_validables}, # tri ) - if mode_jury and self.validations: - if self.is_apc: + if table.mode_jury and res.validations: + if res.is_apc: # formations BUT: pas de code semestre, concatene ceux des UEs - dec_ues = self.validations.decisions_jury_ues.get(etud.id) + dec_ues = res.validations.decisions_jury_ues.get(etud.id) if dec_ues: jury_code_sem = ",".join( [dec_ues[ue_id].get("code", "") for ue_id in dec_ues] @@ -706,58 +833,60 @@ class ResultatsSemestre(ResultatsCache): jury_code_sem = "" else: # formations classiques: code semestre - dec_sem = self.validations.decisions_jury.get(etud.id) + dec_sem = res.validations.decisions_jury.get(etud.id) jury_code_sem = dec_sem["code"] if dec_sem else "" - row.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem") - row.add_cell( + self.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem") + self.add_cell( "jury_link", "", f"""{("saisir" if not jury_code_sem else "modifier") - if self.formsemestre.etat else "voir"} décisions""", + if res.formsemestre.etat else "voir"} décisions""", "col_jury_link", ) - def _recap_add_ue(self, row: tb.Row, ue: UniteEns, ue_status: dict): + def add_ue(self, ue: UniteEns, ue_status: dict): "Ajoute résultat UE au row (colonne col_ue)" - table = row.table + table = self.table col_id = f"moy_ue_{ue.id}" val = ue_status["moy"] note_class = "" if isinstance(val, float): if val < table.barre_moy: - note_class = " moy_inf" + note_class = "moy_inf" elif val >= table.barre_valid_ue: - note_class = " moy_ue_valid" - row.nb_ues_validables += 1 + note_class = "moy_ue_valid" + self.nb_ues_validables += 1 if val < table.barre_warning_ue: - note_class = " moy_ue_warning" # notes très basses - row.nb_ues_warning += 1 - row.add_cell( + note_class = "moy_ue_warning" # notes très basses + self.nb_ues_warning += 1 + self.add_cell( col_id, ue.acronyme, table.fmt_note(val), group=f"col_ue_{ue.id}", classes=["col_ue", note_class], ) - row.table.foot_title_row.cells[col_id].target_attrs[ + table.foot_title_row.cells[col_id].target_attrs[ "title" ] = f"""{ue.titre} S{ue.semestre_idx or '?'}""" - def _recap_add_ue_modimpls( + def add_ue_modimpls( self, row: tb.Row, ue: UniteEns, etud: Identite, is_capitalized: bool ): """Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE""" + # pylint: disable=invalid-unary-operand-type table = row.table - for modimpl in self.modimpls_in_ue(ue, etud.id, with_bonus=False): + res = table.res + for modimpl in res.modimpls_in_ue(ue, etud.id, with_bonus=False): if is_capitalized: val = "-c-" else: - modimpl_results = self.modimpls_results.get(modimpl.id) + modimpl_results = res.modimpls_results.get(modimpl.id) if modimpl_results: # pas bonus - if self.is_apc: # BUT + if res.is_apc: # BUT moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id) val = ( moys_vers_ue.get(etud.id, "?") @@ -772,8 +901,20 @@ class ResultatsSemestre(ResultatsCache): col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" val_fmt = val_fmt_html = table.fmt_note(val) if modimpl.module.module_type == scu.ModuleType.MALUS: - val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" - cell = row.add_cell( + if val and not isinstance(val, str) and not np.isnan(val): + if val >= 0: + val_fmt_html = f"""+{ + val_fmt + }""" + else: + # val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) + val_fmt_html = f"""-{ + table.fmt_note(-val)}""" + + else: + val_fmt = val_fmt_html = "" # inscrit à ce malus, mais sans note + + cell = self.add_cell( col_id, modimpl.module.code, val_fmt_html, @@ -805,121 +946,6 @@ class ResultatsSemestre(ResultatsCache): ] = f"{modimpl.module.titre} ({nom_resp})" table.modimpl_ue_ids.add((modimpl.id, ue.id)) - def _recap_add_bottom_rows(self, table: tb.Table, ues): - """Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo""" - # Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo - row_min = tb.BottomRow( - table, - "min", - left_title="Min.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_max = tb.BottomRow( - table, - "max", - left_title="Max.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_moy = tb.BottomRow( - table, - "moy", - left_title="Moy.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_coef = tb.BottomRow( - table, - "coef", - left_title="Coef.", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_ects = tb.BottomRow( - table, - "ects", - left_title="ECTS", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - row_apo = tb.BottomRow( - table, - "apo", - left_title="Code Apogée", - left_title_col_ids=["prenom", "nom_short"], - category="bottom_infos", - classes=["bottom_info"], - ) - - # --- ECTS - # titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms - for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_ects.add_cell(col_id, None, ue.ects) - # ajoute cell UE vides sur ligne coef pour borders verticales - # XXX TODO classes dans table sur colonne ajoutées à tous les TD - row_coef.add_cell(col_id, None, "") - row_ects.add_cell( - "moy_gen", - None, - sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]), - ) - # --- MIN, MAX, MOY, APO - row_min.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.min())) - row_max.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.max())) - row_moy.add_cell("moy_gen", None, table.fmt_note(self.etud_moy_gen.mean())) - - for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_min.add_cell( - col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].min()) - ) - row_max.add_cell( - col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].max()) - ) - row_moy.add_cell( - col_id, None, table.fmt_note(self.etud_moy_ue[ue.id].mean()) - ) - row_apo.add_cell(col_id, None, ue.code_apogee or "") - - for modimpl in self.formsemestre.modimpls_sorted: - if (modimpl.id, ue.id) in table.modimpl_ue_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] - else: - coef = modimpl.module.coefficient or 0 - row_coef.add_cell( - col_id, - None, - table.fmt_note(coef), - group=f"col_ue_{ue.id}_modules", - ) - notes = self.modimpl_notes(modimpl.id, ue.id) - if np.isnan(notes).all(): - # aucune note valide - row_min.add_cell(col_id, None, np.nan) - row_max.add_cell(col_id, None, np.nan) - moy = np.nan - else: - row_min.add_cell(col_id, None, table.fmt_note(np.nanmin(notes))) - row_max.add_cell(col_id, None, table.fmt_note(np.nanmax(notes))) - moy = np.nanmean(notes) - row_moy.add_cell( - col_id, - None, - table.fmt_note(moy), - # aucune note dans ce module ? - classes=["col_empty" if np.isnan(moy) else ""], - ) - row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "") - def _recap_etud_groups_infos( self, etudid: int, row: dict, titles: dict ): # XXX non utilisé diff --git a/sco_version.py b/sco_version.py index c42cd066..f450245e 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.35" +SCOVERSION = "9.4.37" SCONAME = "ScoDoc"