From 795de44c0cfc09c59387943887010b298323f387 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 3 Apr 2022 16:20:16 +0200 Subject: [PATCH] Tableau recap: bonus et malus --- app/comp/res_common.py | 109 +++++++++++++++++++++++++-------- app/scodoc/sco_recapcomplet.py | 10 +-- app/scodoc/sco_utils.py | 1 + app/static/css/scodoc.css | 38 ++++++++++++ app/static/js/table_recap.js | 21 +++++-- 5 files changed, 143 insertions(+), 36 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index f1020b48..fbeba8be 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -426,47 +426,53 @@ class ResultatsSemestre(ResultatsCache): NO_NOTE = "-" # contenu des cellules sans notes rows = [] # column_id : title - titles = { - "rang": "Rg", - # ordre des colonnes de gauche à droite: - "_civilite_str_col_order": 2, - "_nom_disp_col_order": 3, - "_prenom_col_order": 4, - "_nom_short_col_order": 5, - "_rang_col_order": 6, - # les colonnes des groupes sont à la position 10 - "_ues_validables_col_order": 20, - } + titles = {} # 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 = "" + row: dict, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = 100, ): "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 + row[f"_{col_id}_class"] = classes + f" c{idx}" if not col_id in titles: titles[col_id] = title + titles[f"_{col_id}_col_order"] = idx if classes: titles[f"_{col_id}_class"] = classes + return idx + 1 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] modimpl_ids = set() # modimpl effectivement présents dans la table for etudid in etuds_inscriptions: + idx = 0 # index de la colonne etud = Identite.query.get(etudid) row = {"etudid": etudid} # --- Rang - add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang") + idx = add_cell( + row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx + ) 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") + idx = add_cell( + row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx + ) + idx = add_cell( + row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail", idx + ) + idx = add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail", idx) + idx = add_cell( + row, "nom_short", "Nom", etud.nom_short, "identite_court", idx + ) row["_nom_short_target"] = url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, @@ -476,6 +482,8 @@ class ResultatsSemestre(ResultatsCache): 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"] + + idx = 30 # début des colonnes de notes # --- Moyenne générale moy_gen = self.etud_moy_gen.get(etudid, False) note_class = "" @@ -483,12 +491,13 @@ class ResultatsSemestre(ResultatsCache): moy_gen = NO_NOTE elif isinstance(moy_gen, float) and moy_gen < barre_moy: note_class = " moy_ue_warning" # en rouge - add_cell( + idx = add_cell( row, "moy_gen", "Moy", fmt_note(moy_gen), "col_moy_gen" + note_class, + idx, ) titles_bot["_moy_gen_target_attrs"] = ( 'title="moyenne indicative"' if self.is_apc else "" @@ -510,16 +519,32 @@ class ResultatsSemestre(ResultatsCache): if val < barre_warning_ue: note_class = " moy_ue_warning" # notes très basses nb_ues_warning += 1 - add_cell( + idx = add_cell( row, col_id, ue.acronyme, fmt_note(val), "col_ue" + note_class, + idx, ) titles_bot[ f"_{col_id}_target_attrs" ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ + # 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 "" + val_fmt = fmt_note(val) + if val: + val_fmt = f'{val_fmt}' + idx = add_cell( + row, + f"bonus_ue_{ue.id}", + f"Bonus {ue.acronyme}", + val_fmt, + "col_ue_bonus", + idx, + ) # 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"]: @@ -546,13 +571,19 @@ class ResultatsSemestre(ResultatsCache): col_id = ( f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" ) - add_cell( + val_fmt = fmt_note(val) + if modimpl.module.module_type == scu.ModuleType.MALUS: + val_fmt = ( + (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" + ) + idx = add_cell( row, col_id, modimpl.module.code, - fmt_note(val), + val_fmt, # class col_res mod_ue_123 f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", + idx, ) titles_bot[f"_{col_id}_target"] = url_for( "notes.moduleimpl_status", @@ -571,9 +602,10 @@ class ResultatsSemestre(ResultatsCache): add_cell( row, "ues_validables", - "Nb UE", + "UEs", ue_valid_txt, "col_ue col_ues_validables", + 29, # juste avant moy. gen. ) if nb_ues_warning: row["_ues_validables_class"] += " moy_ue_warning" @@ -581,6 +613,7 @@ class ResultatsSemestre(ResultatsCache): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri rows.append(row) + self._recap_add_partitions(rows, titles) self._recap_add_admissions(rows, titles) # tri par rang croissant @@ -589,6 +622,16 @@ class ResultatsSemestre(ResultatsCache): # INFOS POUR FOOTER bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) + # Ajoute style "col_empty" aux colonnes de modules vides + for col_id in titles: + c_class = f"_{col_id}_class" + if "col_empty" in bottom_infos["moy"].get(c_class, ""): + for row in rows: + row[c_class] += " col_empty" + titles[c_class] += " col_empty" + for row in bottom_infos.values(): + row[c_class] = row.get(c_class, "") + " col_empty" + # --- TABLE FOOTER: ECTS, moyennes, min, max... footer_rows = [] for (bottom_line, row) in bottom_infos.items(): @@ -604,7 +647,9 @@ class ResultatsSemestre(ResultatsCache): titles_bot.update(titles) footer_rows.append(titles_bot) column_ids = [title for title in titles if not title.startswith("_")] - column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 100)) + column_ids.sort( + key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000) + ) return (rows, footer_rows, titles, column_ids) def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: @@ -651,7 +696,11 @@ class ResultatsSemestre(ResultatsCache): 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)) + moy = np.nanmean(notes) + row_moy[col_id] = fmt_note(moy) + if np.isnan(moy): + # aucune note dans ce module + row_moy[f"_{col_id}_class"] = "col_empty" return { # { key : row } avec key = min, max, moy, coef "min": row_min, @@ -696,6 +745,14 @@ class ResultatsSemestre(ResultatsCache): "type_admission": "Type Adm.", "classement": "Rg. Adm.", } + first = True + for i, cid in enumerate(fields): + titles[f"_{cid}_col_order"] = 10000 + i # tout à droite + if first: + titles[f"_{cid}_class"] = "admission admission_first" + first = False + else: + titles[f"_{cid}_class"] = "admission" titles.update(fields) for row in rows: etud = Identite.query.get(row["etudid"]) @@ -708,8 +765,6 @@ class ResultatsSemestre(ResultatsCache): first = False else: row[f"_{cid}_class"] = "admission" - titles[f"_{cid}_class"] = row[f"_{cid}_class"] - titles[f"_{cid}_col_order"] = 1000 # à la fin def _recap_add_partitions(self, rows: list[dict], titles: dict): """Ajoute les colonnes indiquant les groupes diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 117ce567..a00e7df3 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -643,7 +643,7 @@ def make_formsemestre_recapcomplet( "recap_row_nbeval", "recap_row_ects", )[ir - nblines + 6] - cells = '' % styl + cells = f'' else: el = etudlink % { "formsemestre_id": formsemestre_id, @@ -651,14 +651,14 @@ def make_formsemestre_recapcomplet( "name": l[1], } if ir % 2 == 0: - cells = '' % etudid + cells = f'' else: - cells = '' % etudid + cells = f'' ir += 1 # XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ] # notes sans le NA: nsn = l[:-2] # copy - for i in range(len(nsn)): + for i, _ in enumerate(nsn): if nsn[i] == "NA": nsn[i] = "-" try: @@ -1041,7 +1041,7 @@ def gen_formsemestre_recapcomplet_html( "", ) H = [ - f"""
""" + f"""
""" ] # header H.append( diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 504343a4..a332037f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -933,6 +933,7 @@ ICON_XLS = icontag("xlsicon_img", title="Version tableur") # HTML emojis EMO_WARNING = "⚠️" # warning /!\ +EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down def sort_dates(L, reverse=False): diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 6ece2dcf..f393ac46 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3568,6 +3568,11 @@ table.table_recap tbody td:hover { text-decoration: dashed underline; } +/* col moy gen en gras seulement pour les form. classiques */ +table.table_recap.classic td.col_moy_gen { + font-weight: bold; +} + table.table_recap .identite_court { white-space: nowrap; text-align: left; @@ -3637,6 +3642,39 @@ table.table_recap td.col_ues_validables { font-style: normal !important; } + +.green-arrow-up { + display: inline-block; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid rgb(48, 239, 0); +} + +table.table_recap td.col_ue_bonus, +table.table_recap th.col_ue_bonus { + font-size: 80%; + font-weight: bold; + color: rgb(0, 128, 11); +} + +table.table_recap td.col_ue_bonus>span.sp2l { + margin-left: 2px; +} + +table.table_recap td.col_ue_bonus { + white-space: nowrap; +} + +table.table_recap td.col_malus, +table.table_recap th.col_malus { + font-size: 80%; + font-weight: bold; + color: rgb(165, 0, 0); +} + + table.table_recap tr.ects td { color: rgb(160, 86, 3); font-weight: bold; diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 04acb30e..803daee2 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -29,15 +29,19 @@ $(function () { action: function (e, dt, node, config) { let visible = dt.columns(".col_res").visible()[0]; dt.columns(".col_res").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); } } : { name: "toggle_mod", text: "Cacher les modules", action: function (e, dt, node, config) { - let visible = dt.columns(".col_mod").visible()[0]; - dt.columns(".col_mod").visible(!visible); + let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; + dt.columns(".col_mod:not(.col_empty)").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules"); + visible = dt.columns(".col_empty").visible()[0]; + dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides"); } } ]; @@ -62,6 +66,15 @@ $(function () { dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); } }) + buttons.push({ + name: "toggle_col_empty", + text: "Montrer mod. vides", + action: function (e, dt, node, config) { + let visible = dt.columns(".col_empty").visible()[0]; + dt.columns(".col_empty").visible(!visible); + dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides"); + } + }) $('table.table_recap').DataTable( { paging: false, @@ -77,8 +90,8 @@ $(function () { colReorder: true, "columnDefs": [ { - // cache le détail de l'identité et les colonnes admission - "targets": ["identite_detail", "partition_aux", "admission"], + // cache le détail de l'identité, les groupes, les colonnes admission et les vides + "targets": ["identite_detail", "partition_aux", "admission", "col_empty"], "visible": false, }, ],