From d3672423dbde19b80f16798496fd7e47bbcc60d8 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
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 def9c9b3..11dd02f9 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"""<form name="f" method="get" action="{request.base_url}">
+ f"""<form id="export_menu" name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
+ <input type="hidden" id="visible_col_ids" name="visible_col_ids" value=""></input>
"""
)
if mode_jury:
@@ -133,13 +137,14 @@ def formsemestre_recapcomplet(
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
)
H.append(
- '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
+ '<select name="tabformat" id="tabformat" onchange="submit_from_export_menu();" class="noprint">'
)
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 8b861b75..6bfa63aa 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 cec24081..466ac8e8 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):
--
GitLab