diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 4d8e5922b5791a426a49b480a80776a03b16f3a1..12225fd8e31db53e8a71778d5e3406581a90e6c8 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -4,7 +4,7 @@ # See LICENSE ############################################################################## -"""Jury BUT: table recap annuelle et liens saisie +"""Jury BUT et classiques: table recap annuelle et liens saisie """ import collections @@ -19,15 +19,15 @@ from app.but.jury_but import ( DecisionsProposeesUE, ) from app.comp.res_but import ResultatsSemestreBUT +from app.comp.res_compat import NotesTableCompat from app.comp import res_sem +from app.models import UniteEns 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 @@ -39,16 +39,97 @@ from app.tables.recap import RowRecap, TableRecap class TableJury(TableRecap): """Cette table recap reprend les colonnes du tableau recap, sauf les évaluations, et ajoute: - - les RCUEs - - le lien de saisie ou modif de la décision de jury + Pour le BUT: + - les RCUEs (moyenne et code décision) + Pour toutes les formations: + - les codes de décisions jury sur les UEs + - le lien de saisie ou modif de la décision de jury """ + def __init__(self, *args, row_class: str = None, read_only=True, **kwargs): + super().__init__( + *args, row_class=row_class or RowJury, finalize=False, **kwargs + ) + # redéclare pour VSCode + self.rows: list["RowJury"] = self.rows + self.res: NotesTableCompat = self.res + self.read_only = read_only + # Stats jury: fréquence de chaque code enregistré + self.freq_codes_annuels = collections.Counter() + # Ajout colonnes spécifiques à la table jury: + + if self.res.is_apc: + self.add_rcues() + self.add_jury() + # Termine la table + self.finalize() + self.add_groups_header() + + def add_rcues(self): + """Ajoute les colonnes indiquant le nb de RCUEs et chaque RCUE + pour tous les étudiants de la table. + La table contient des rows avec la clé etudid. + Les colonnes ont la classe css "rcue". + """ + self.insert_group("rcue", before="col_ues_validables") + for row in self.rows: + etud: Identite = row.etud + deca = row.deca + if deca.code_valide: + self.freq_codes_annuels[deca.code_valide] += 1 + row.add_nb_rcues_cell() + # --- 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_rcue_cols(dec_rcue) + + def add_jury(self): + """Ajoute la colonne code jury et le lien. + Le code jury est celui du semestre: cette colonne n'est montrée + que pour les formations classiques, ce code n'est pas utilisé en BUT. + """ + res = self.res + if res.validations: + for row in self.rows: + etud = row.etud + if not res.is_apc: + # formations classiques: code semestre + 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, group="jury_code_sem" + ) + row.add_cell( + "jury_link", + "", + f"""{("modifier" if res.validations.has_decision(etud) else "saisir") + if res.formsemestre.etat else "voir"} décisions""", + group="col_jury_link", + target=url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=res.formsemestre.id, + etudid=etud.id, + ), + target_attrs={"class": "stdlink"}, + ) + class RowJury(RowRecap): "Ligne de la table saisie jury" - def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): + def __init__(self, table: TableJury, etud: Identite, *args, **kwargs): + self.table: TableJury = table + super().__init__(table, etud, *args, **kwargs) + # Conserve le deca de cet étudiant: + self.deca = jury_but.DecisionsProposeesAnnee( + self.etud, self.table.res.formsemestre + ) + + def add_nb_rcues_cell(self): "cell avec nb niveaux validables / total" + deca = self.deca classes = ["col_rcue", "col_rcues_validables"] if deca.nb_rcues_under_8 > 0: classes.append("moy_ue_warning") @@ -78,42 +159,71 @@ class RowJury(RowRecap): "RCUEs", f"""{deca.nb_validables}/{deca.nb_competences}""" + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), - group="rcues_validables", + raw_content=f"""{deca.nb_validables}/{deca.nb_competences}""", + group="rcue", 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}" + def add_ue_cols(self, ue: UniteEns, ue_status: dict): + "Ajoute 2 colonnes: moyenne d'UE et code jury" + super().add_ue_cols(ue, ue_status) # table recap standard + dues = self.table.res.get_etud_decision_ues(self.etud.id) + if not dues: + return + due = dues.get(ue.id) + if not due: + return + col_id = f"moy_ue_{ue.id}_code" + self.add_cell( + col_id, + "", # titre vide + due["code"], + raw_content=due["code"], + group="col_ue", + classes=["recorded_code"], + column_classes={"col_jury", "col_ue_code"}, + target_attrs={ + "title": f"""enregistrée le {due['event_date']}, { + (due["ects"] or 0):.3g} ECTS.""" + }, + ) + + def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE): + "2 cells: moyenne du RCUE, code enregistré" + self.table.group_titles["rcue"] = "RCUEs en cours" + rcue = dec_rcue.rcue + col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id note_class = "" - val = dec_ue.moy_ue + val = rcue.moy_rcue if isinstance(val, float): - if val < BUT_BARRE_UE: - note_class = "moy_inf" - elif val >= BUT_BARRE_UE: + if val < BUT_BARRE_RCUE: + note_class = "moy_ue_inf" + elif val >= BUT_BARRE_RCUE: note_class = "moy_ue_valid" - if val < BUT_BARRE_UE8: + if val < BUT_RCUE_SUFFISANT: 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", - classes="col_ue" + note_class, - column_class="col_ue", + f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>", + self.table.fmt_note(val), + raw_content=val, + group="rcue", + classes=[note_class], + column_classes={"col_rcue"}, ) self.add_cell( col_id + "_code", - dec_ue.ue.acronyme, - dec_ue.code_valide or "", - classes="col_ue_code recorded_code", - column_class="col_ue", + f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>", + dec_rcue.code_valide or "", + group="rcue", + classes=["col_rcue_code", "recorded_code"], + column_classes={"col_rcue"}, ) def formsemestre_saisie_jury_but( - formsemestre2: FormSemestre, + formsemestre: FormSemestre, read_only: bool = False, selected_etudid: int = None, mode="jury", @@ -125,7 +235,6 @@ def formsemestre_saisie_jury_but( 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 @@ -137,32 +246,40 @@ def formsemestre_saisie_jury_but( # 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) + if formsemestre.formation.referentiel_competence is None: + raise ScoNoReferentielCompetences(formation=formsemestre.formation) - rows, titles, column_ids, jury_stats = get_jury_but_table( - formsemestre2, read_only=read_only, mode=mode + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) + table = TableJury( + res, + convert_values=True, + mode_jury=True, + read_only=read_only, + classes=[ + "table_jury_but_bilan" if mode == "recap" else "", + "table_recap", + "apc", + "jury table_jury_but", + ], + selected_row_id=selected_etudid, ) - if not rows: + if table.is_empty(): return ( '<div class="table_recap"><div class="message">aucun étudiant !</div></div>' ) - 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 + table.data["filename"] = scu.sanitize_filename( + f"""jury-but-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) + table_html = table.html() H = [ html_sco_header.sco_header( - page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel", + page_title=f"{formsemestre.sem_modalite()}: jury BUT", 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 + formsemestre_id=formsemestre.id ), ] if mode == "recap": @@ -173,12 +290,12 @@ def formsemestre_saisie_jury_but( <ul> <li><a href="{url_for( "notes.pvjury_table_but", - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }" class="stdlink">Tableau PV de jury</a> </li> <li><a href="{url_for( "notes.formsemestre_lettres_individuelles", - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }" class="stdlink">Courriers individuels (classeur pdf)</a> </li> </div> @@ -187,9 +304,10 @@ def formsemestre_saisie_jury_but( ) H.append( f""" - + <div class="table_recap"> {table_html} - + </div> + <div class="table_jury_but_links"> """ ) @@ -199,7 +317,7 @@ def formsemestre_saisie_jury_but( f""" <p><a class="stdlink" href="{url_for( "notes.formsemestre_saisie_jury", - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }">Saisie des décisions du jury</a> </p>""" ) @@ -208,12 +326,12 @@ def formsemestre_saisie_jury_but( f""" <p><a class="stdlink" href="{url_for( "notes.formsemestre_validation_auto_but", - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }">Calcul automatique des décisions du jury</a> </p> <p><a class="stdlink" href="{url_for( "notes.formsemestre_jury_but_recap", - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }">Tableau récapitulatif des décisions du jury</a> </p> """ @@ -224,19 +342,19 @@ def formsemestre_saisie_jury_but( <div class="jury_stats"> <div>Nb d'étudiants avec décision annuelle: - {sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]} + {sum(table.freq_codes_annuels.values())} / {len(table)} </div> <div><b>Codes annuels octroyés:</b></div> <table class="jury_stats_codes"> """ ) - for code in sorted(jury_stats["codes_annuels"].keys()): + for code in sorted(table.freq_codes_annuels.keys()): H.append( f"""<tr> <td>{code}</td> - <td style="text-align:right">{jury_stats["codes_annuels"][code]}</td> + <td style="text-align:right">{table.freq_codes_annuels[code]}</td> <td style="text-align:right">{ - (100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}% + (100*table.freq_codes_annuels[code] / len(table)):2.1f}% </td> </tr>""" ) @@ -346,43 +464,16 @@ class RowCollector: self.column_classes[col_id] = column_class self.idx += 1 - 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"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>", - self.fmt_note(val), - "col_rcue" + note_class, - column_class="col_rcue", - ) - self.add_cell( - col_id + "_code", - f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>", - dec_rcue.code_valide or "", - "col_rcue_code recorded_code", - column_class="col_rcue", - ) - -def get_jury_but_table( +def get_jury_but_table( # XXX A SUPPRIMER apres avoir recupéré les stats 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) + + # /////// XXX /////// XXX ////// titles = {} # column_id : title jury_stats = { "nb_etuds": len(formsemestre2.etuds_inscriptions), diff --git a/app/comp/jury.py b/app/comp/jury.py index e41af510786eab31111ec5dae33df5a55c61979c..226d5a4f014d4d89a187cf3f59aa9a93e8fd56a5 100644 --- a/app/comp/jury.py +++ b/app/comp/jury.py @@ -9,7 +9,7 @@ import pandas as pd from app import db -from app.models import FormSemestre, ScolarFormSemestreValidation, UniteEns +from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns from app.comp.res_cache import ResultatsCache from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours @@ -53,7 +53,7 @@ class ValidationsSemestre(ResultatsCache): self.comp_decisions_jury() def comp_decisions_jury(self): - """Cherche les decisions du jury pour le semestre (pas les UE). + """Cherche les decisions du jury pour le semestre (pas les RCUE). Calcule les attributs: decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} decision_jury_ues={ etudid : @@ -102,6 +102,12 @@ class ValidationsSemestre(ResultatsCache): self.decisions_jury_ues = decisions_jury_ues + def has_decision(self, etud: Identite) -> bool: + """Vrai si etud a au moins une décision enregistrée depuis + ce semestre (quelle qu'elle soit) + """ + return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury) + def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame: """Liste des UE capitalisées (ADM) utilisables dans ce formsemestre diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 58fd7307bbcca57c38d9a87fabfb173db41b5119..c5414db8f2be56dcf38e6f62ccc9e5c6584929d1 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -4020,6 +4020,11 @@ table.table_recap tbody td:hover { text-decoration: dashed underline; } +table.table_recap tfoot tr td { + padding-left: 10px; + padding-right: 10px; +} + /* col moy gen en gras seulement pour les form. classiques */ table.table_recap.classic td.col_moy_gen { font-weight: bold; @@ -4039,10 +4044,10 @@ table.table_recap .cursus { white-space: nowrap; } -table.table_recap .col_ue, -table.table_recap .col_ue_code, -table.table_recap .col_moy_gen, -table.table_recap .group { +table.table_recap td.col_ue, +table.table_recap td.col_ue_code, +table.table_recap td.col_moy_gen, +table.table_recap td.group { border-left: 1px solid blue; } @@ -4050,7 +4055,7 @@ table.table_recap .col_ue { font-weight: bold; } -table.table_recap.jury .col_ue { +table.table_recap.jury td.col_ue { font-weight: normal; } @@ -4059,29 +4064,53 @@ table.table_recap.jury .col_rcue_code { font-weight: bold; } -table.table_recap.jury tr.even td.col_rcue, -table.table_recap.jury tr.even td.col_rcue_code { - background-color: #b0d4f8; -} - table.table_recap.jury tr.odd td.col_rcue, table.table_recap.jury tr.odd td.col_rcue_code { - background-color: #abcdef; + background-color: #e0eeff; } -table.table_recap.jury tr.odd td.col_rcues_validables { - background-color: #e1d3c5 !important; +/* table.table_recap.jury tr.even td.col_rcue, +table.table_recap.jury tr.even td.col_rcue_code { + background-color: #e5f2ff; +} */ + +/* table.table_recap.jury tr.odd td.col_rcues_validables { + background-color: #d5eaff !important; } table.table_recap.jury tr.even td.col_rcues_validables { - background-color: #fcebda !important; -} + background-color: #e5f2ff !important; +} */ + + table.table_recap .group { border-left: 1px dashed rgb(160, 160, 160); white-space: nowrap; } +table.table_recap thead th { + border-left: 1px solid rgb(200, 200, 200); + border-right: 1px solid rgb(200, 200, 200); +} + +table.table_recap tr.groups_header th { + border-bottom: none; + font-weight: normal; + font-style: italic; + text-align: center; + padding-bottom: 8px; +} + +table.table_recap thead tr.titles th { + padding-top: 0px; +} + +table.table_recap tr td.col_ue_code, +table.table_recap tr th.col_ue_code { + border-left: none; +} + table.table_recap .admission { white-space: nowrap; color: rgb(6, 73, 6); @@ -4250,7 +4279,7 @@ table.table_recap tr.apo td { max-width: 1px; } -table.table_recap tr.type_col { +table.table_recap tr.type_col td { font-size: 40%; font-family: monospace; overflow-wrap: anywhere; diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 4b45961df8b1cabddd54e863ba95de5076fab640..9597f9dbb4b0b3f11e543d6c6423efe3366f72e7 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -10,8 +10,6 @@ $(function () { if (mode_jury_but_bilan) { // table bilan décisions: cache les notes hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]); - } else { - hidden_colums = hidden_colums.concat(["recorded_code"]); } // Etat (tri des colonnes) de la table: diff --git a/app/tables/recap.py b/app/tables/recap.py index 800adda1bb357fbad2dd8c1de4d2646622ff45e5..e405e987322ffb7ee7362fb0cfe54d3e36e3be53 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -54,8 +54,12 @@ class TableRecap(tb.Table): convert_values=False, include_evaluations=False, mode_jury=False, + row_class=None, + finalize=True, + **kwargs, ): - super().__init__() + self.rows: list["RowRecap"] = [] # juste pour que VSCode nous aide sur .rows + super().__init__(row_class=row_class or RowRecap, **kwargs) self.res = res self.include_evaluations = include_evaluations self.mode_jury = mode_jury @@ -72,13 +76,12 @@ class TableRecap(tb.Table): # couples (modimpl, ue) effectivement présents dans la table: self.modimpl_ue_ids = set() - etuds_inscriptions = res.formsemestre.etuds_inscriptions ues = res.formsemestre.query_ues(with_sport=True) # avec bonus ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT] - for etudid in etuds_inscriptions: + for etudid in res.formsemestre.etuds_inscriptions: etud = Identite.query.get(etudid) - row = RowRecap(self, etud) + row = self.row_class(self, etud) self.add_row(row) row.add_etud_cols() row.add_moyennes_cols(ues_sans_bonus) @@ -100,11 +103,14 @@ class TableRecap(tb.Table): if include_evaluations: self.add_evaluations() + def finalize(self): + """Termine la table: ajoute ligne avec les types, + et ajoute classe sur les colonnes vides""" self.mark_empty_cols() self.add_type_row() def mark_empty_cols(self): - """Ajoute style "col_empty" aux colonnes de modules vides""" + """Ajoute classe "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: @@ -275,11 +281,12 @@ class TableRecap(tb.Table): def add_partitions(self): """Ajoute les colonnes indiquant les groupes + pour tous les étudiants de la table. La table contient des rows avec la clé etudid. - - Les colonnes ont la classe css "partition" + Les colonnes ont la classe css "partition". """ self.insert_group("partition", after="identite_court") + self.group_titles["partition"] = "Partitions" partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( self.res.formsemestre.id ) @@ -404,6 +411,7 @@ class TableRecap(tb.Table): scodoc_dept=g.scodoc_dept, evaluation_id=e.id, ), + target_attrs={"class": "stdlink"}, ) def add_admissions(self): @@ -478,6 +486,13 @@ class RowRecap(tb.Row): """Ajoute colonnes étudiant: codes, noms""" res = self.table.res etud = self.etud + self.table.group_titles.update( + { + "etud_codes": "Codes", + "identite_detail": "", + "identite_court": "", + } + ) # --- Codes (seront cachés, mais exportés en excel) self.add_cell("etudid", "etudid", etud.id, "etud_codes") self.add_cell( @@ -614,35 +629,11 @@ class RowRecap(tb.Row): data={"order": self.nb_ues_validables}, # tri ) - if table.mode_jury and res.validations: - if res.is_apc: - # formations BUT: pas de code semestre, concatene ceux des UEs - 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] - ) - else: - jury_code_sem = "" - else: - # formations classiques: code semestre - dec_sem = res.validations.decisions_jury.get(etud.id) - jury_code_sem = dec_sem["code"] if dec_sem else "" - self.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem") - self.add_cell( - "jury_link", - "", - f"""<a href="{url_for('notes.formsemestre_validation_etud_form', - scodoc_dept=g.scodoc_dept, formsemestre_id=res.formsemestre.id, etudid=etud.id - ) - }">{("saisir" if not jury_code_sem else "modifier") - if res.formsemestre.etat else "voir"} décisions</a>""", - "col_jury_link", - ) - def add_ue_cols(self, ue: UniteEns, ue_status: dict): "Ajoute résultat UE au row (colonne col_ue)" + # sous-classé par JuryRow pour ajouter les codes table = self.table + table.group_titles["col_ue"] = "UEs du semestre" col_id = f"moy_ue_{ue.id}" val = ue_status["moy"] note_class = "" @@ -659,9 +650,9 @@ class RowRecap(tb.Row): col_id, ue.acronyme, table.fmt_note(val), - group=f"col_ue_{ue.id}", + group="col_ue", classes=[note_class], - column_classes={"col_ue", "col_moy_ue"}, + column_classes={f"col_ue_{ue.id}", "col_moy_ue"}, ) table.foot_title_row.cells[col_id].target_attrs[ "title" diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py index f500fb0f14c84bad52519cdd538801d199425876..19e14ea17c0655fa937fd8d9410b391bff777d97 100644 --- a/app/tables/table_builder.py +++ b/app/tables/table_builder.py @@ -73,8 +73,10 @@ class Table(Element): classes: list[str] = None, attrs: dict[str, str] = None, data: dict = None, + row_class=None, ): super().__init__("table", classes=classes, attrs=attrs, data=data) + self.row_class = row_class or Row self.rows: list["Row"] = [] "ordered list of Rows" self.row_by_id: dict[str, "Row"] = {} @@ -82,6 +84,8 @@ class Table(Element): "ordered list of columns ids" self.groups = [] "ordered list of column groups names" + self.group_titles = {} + "title (in header top row) for the group" self.head = [] self.foot = [] self.column_group = {} @@ -92,8 +96,12 @@ class Table(Element): "l'id de la ligne sélectionnée" self.titles = {} "Column title: { col_id : titre }" - self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th") - self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th") + self.head_title_row: "Row" = Row( + self, "title_head", cell_elt="th", classes=["titles"] + ) + self.foot_title_row: "Row" = Row( + self, "title_foot", cell_elt="th", classes=["titles"] + ) self.empty_cell = Cell.empty() def _prepare(self): @@ -109,6 +117,10 @@ class Table(Element): "return the row, or None" return self.row_by_id.get(row_id) + def __len__(self): + "nombre de lignes dans le corps de la table" + return len(self.rows) + def is_empty(self) -> bool: "true if table has no rows" return len(self.rows) == 0 @@ -166,7 +178,6 @@ class Table(Element): def add_head_row(self, row: "Row") -> "Row": "Add a row to table head" - # row = Row(self, cell_elt="th", category="head") self.head.append(row) self.row_by_id[row.id] = row return row @@ -177,6 +188,16 @@ class Table(Element): self.row_by_id[row.id] = row return row + def add_groups_header(self): + """Insert a header line at the top of the table + with a multicolumn th cell per group + """ + self.sort_columns() + groups_header = RowGroupsHeader( + self, "groups_header", classes=["groups_header"] + ) + self.head.insert(0, groups_header) + def sort_rows(self, key: callable, reverse: bool = False): """Sort table rows""" self.rows.sort(key=key, reverse=reverse) @@ -418,3 +439,33 @@ class Cell(Element): return f"<a {href} {target_attrs_str}>{super().html_content()}</a>" return super().html_content() + + +class RowGroupsHeader(Row): + """Header line at the top of the table + with a multicolumn th cell per group + """ + + def html_content(self): + """Le contenu est généré intégralement ici: un th par groupe contigu + Note: les colonnes doivent avoir déjà été triées par table.sort_columns() + """ + column_ids = self.table.column_ids + nb_cols = len(self.table.column_ids) + elements = [] + idx = 0 + while idx < nb_cols: + group_title = self.table.group_titles.get( + self.table.column_group.get(column_ids[idx]) + ) + colspan = 1 + idx += 1 + # on groupe tant que les TITRES des groupes sont identiques + while idx < nb_cols and group_title == self.table.group_titles.get( + self.table.column_group.get(column_ids[idx]) + ): + idx += 1 + colspan += 1 + elements.append(f"""<th colspan="{colspan}">{group_title or ""}</th>""") + + return "\n".join(elements) if len(elements) > 1 else "" diff --git a/app/views/notes.py b/app/views/notes.py index d5b9bca9fc0609866725f840258ad0448b36f5c8..4ae34dd7688ac903041a4865857d5ef4aa45ab54 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2891,7 +2891,7 @@ def formsemestre_jury_but_erase( return render_template( "confirm_dialog.j2", title=f"""Effacer les validations de jury { - ("de" + etud.nomprenom) + ("de " + etud.nomprenom) if etud else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre") } ?""",