From d3672423dbde19b80f16798496fd7e47bbcc60d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 14 Dec 2024 17:22:30 +0100 Subject: [PATCH] Export excel table recap avec colonnes choisies en HTML --- app/scodoc/sco_recapcomplet.py | 24 +++++++++++++++++++----- app/static/js/table_recap.js | 28 ++++++++++++++++++++++++++++ app/tables/table_builder.py | 34 ++++++++++++++++++++++------------ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index def9c9b30..11dd02f91 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -63,6 +63,7 @@ def formsemestre_recapcomplet( xml_with_decisions=False, force_publishing=True, selected_etudid=None, + visible_col_ids=None, ): """Page récapitulant les notes d'un semestre. Grand tableau récapitulatif avec toutes les notes de modules @@ -86,7 +87,7 @@ def formsemestre_recapcomplet( if not isinstance(formsemestre_id, int): abort(404) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} + file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xlsvisible", "xml"} supported_formats = file_formats | {"html", "evals"} if tabformat not in supported_formats: raise ScoValueError(f"Format non supporté: {tabformat}") @@ -94,6 +95,7 @@ def formsemestre_recapcomplet( mode_jury = int(mode_jury) xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) + visible_col_ids = visible_col_ids.split(",") if visible_col_ids else None filename = scu.sanitize_filename( f"""{'jury' if mode_jury else 'recap' }{'-evals' if tabformat == 'xlsall' else '' @@ -107,6 +109,7 @@ def formsemestre_recapcomplet( filename=filename, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, + visible_col_ids=visible_col_ids, ) table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html( @@ -124,8 +127,9 @@ def formsemestre_recapcomplet( ] if len(formsemestre.inscriptions) > 0: H.append( - f"""
+ f""" + """ ) if mode_jury: @@ -133,13 +137,14 @@ def formsemestre_recapcomplet( f'' ) H.append( - '' ) for fmt, label in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), ("xlsx", "Excel (non formaté)"), ("xlsall", "Excel avec évaluations"), + ("xlsvisible", "Excel avec colonnes telles affichées"), ("json", "Bulletins JSON"), ): if fmt == tabformat: @@ -314,16 +319,19 @@ def _formsemestre_recapcomplet_to_file( xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) xml_with_decisions=False, force_publishing=True, + visible_col_ids=None, ): """Calcule et renvoie le tableau récapitulatif.""" if tabformat.startswith("xls"): include_evaluations = tabformat == "xlsall" + visible_col_ids = visible_col_ids if tabformat == "xlsvisible" else None res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) data, filename = gen_formsemestre_recapcomplet_excel( res, mode_jury=mode_jury, include_evaluations=include_evaluations, filename=filename, + visible_col_ids=visible_col_ids, ) mime, suffix = scu.get_mime_suffix("xlsx") return scu.send_file(data, filename=filename, mime=mime, suffix=suffix) @@ -537,11 +545,13 @@ def gen_formsemestre_recapcomplet_excel( mode_jury: bool = False, include_evaluations=False, filename: str = "", + visible_col_ids: list[str] | None = None, ) -> tuple: """Génère le tableau recap ou jury en excel (xlsx). Utilisé pour menu (export excel), archives et autres besoins particuliers (API). Attention: le tableau exporté depuis la page html est celui généré en js par DataTables, et non celui-ci. + Si visible_col_ids est non None, ne génère que les colonnes indiquées (+ les codes étudiants, toujours présents) """ # En excel, ajoute les adresses mail, si on a le droit de les voir. table = _gen_formsemestre_recapcomplet_table( @@ -552,5 +562,9 @@ def gen_formsemestre_recapcomplet_excel( convert_values=False, filename=filename, ) - - return table.excel(), filename + if visible_col_ids is not None: + # Ajoute colonnes qui doivent toujours être présentes en excel: + for cod in ("code_nip", "etudid"): + if cod not in visible_col_ids: + visible_col_ids = [cod] + visible_col_ids + return table.excel(col_ids=visible_col_ids), filename diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 8b861b757..6bfa63aa2 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -326,3 +326,31 @@ $(function () { } }); }); + +// liste des id de colonnes visibles, dans leur ordre d'affichage +// (chaine avec ids séparés par des virgules) +function get_visible_column_ids() { + const table = $("table.table_recap").DataTable(); + const visibles = table.columns().visible(); + let col_ids = ""; + for (i=0; i < visibles.length; i++) { + if (visibles[i]) { + let th = table.column(i).header(); + if (col_ids.length) { + col_ids += ","; + } + col_ids += th.dataset.col_id; + } + } + return col_ids; +} + +function submit_from_export_menu() { + let form = document.querySelector("#export_menu"); + let tabformat = document.getElementById("tabformat").value; + if (tabformat == "xlsvisible") { + let cols_input = document.getElementById("visible_col_ids"); + cols_input.value = get_visible_column_ids(); + } + form.submit(); +} \ No newline at end of file diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py index cec24081e..466ac8e8a 100644 --- a/app/tables/table_builder.py +++ b/app/tables/table_builder.py @@ -90,8 +90,8 @@ class Table(Element): "ordered list of column groups names" self.group_titles = {} "title (in header top row) for the group" - self.head = [] - self.foot = [] + self.head: list["Row"] = [] + self.foot: list["Row"] = [] self.column_group = {} "the group of the column: { col_id : group }" self.column_classes: defaultdict[str, set[str]] = defaultdict(set) @@ -281,6 +281,7 @@ class Table(Element): col_id, None, title, + attrs={"data-col_id": col_id}, classes=classes, group=self.column_group.get(col_id), raw_content=raw_title or title, @@ -297,8 +298,10 @@ class Table(Element): foot_cell = self.foot_title_row.cells[col_id] if self.foot_title_row else None return head_cell, foot_cell - def excel(self, wb: Workbook = None): - """Simple Excel representation of the table.""" + def excel(self, wb: Workbook = None, col_ids=None): + """Simple Excel representation of the table. + Si col_ids(liste d'ids) est spécifié, ne génère que ces colonnes, dans l'ordre. + """ self._prepare() if wb is None: sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) @@ -309,13 +312,13 @@ class Table(Element): style_base = self.xls_style_base or sco_excel.excel_make_style() for row in self.head: - sheet.append_row(row.to_excel(sheet, style=style_bold)) + sheet.append_row(row.to_excel(sheet, style=style_bold, col_ids=col_ids)) for row in self.rows: - sheet.append_row(row.to_excel(sheet, style=style_base)) + sheet.append_row(row.to_excel(sheet, style=style_base, col_ids=col_ids)) for row in self.foot: - sheet.append_row(row.to_excel(sheet, style=style_base)) + sheet.append_row(row.to_excel(sheet, style=style_base, col_ids=col_ids)) if self.caption: sheet.append_blank_row() # empty line @@ -325,9 +328,10 @@ class Table(Element): sheet.append_single_cell_row(self.origin, style_base) # Largeurs des colonnes + actual_col_ids = col_ids if col_ids else self.column_ids for col_id, width in self.xls_columns_width.items(): try: - idx = self.column_ids.index(col_id) + idx = actual_col_ids.index(col_id) col = get_column_letter(idx + 1) sheet.set_column_dimension_width(col, width) except ValueError: @@ -365,7 +369,7 @@ class Row(Element): title: str, content: str, group: str = None, - attrs: list[str] = None, + attrs: dict[str, str] = None, classes: list[str] = None, data: dict[str, str] = None, elt: str = None, @@ -466,9 +470,15 @@ class Row(Element): for col_id in self.table.raw_column_ids } - def to_excel(self, sheet, style=None) -> list: - "build excel row for given sheet" - return sheet.make_row(self.to_dict().values(), style=style) + def to_excel(self, sheet, style=None, col_ids=None) -> list: + """Build excel row for given sheet. + If col_ids is given, generate only this columns + """ + if col_ids is None: + return sheet.make_row(self.to_dict().values(), style=style) + # Version avec seulement colonnes spécifiées: + d = self.to_dict() + return sheet.make_row([d[k] for k in col_ids if k in d], style=style) class BottomRow(Row):