Export excel table recap avec colonnes choisies en HTML

This commit is contained in:
Emmanuel Viennet 2024-12-14 17:22:30 +01:00
parent cf78081234
commit d3672423db
3 changed files with 69 additions and 17 deletions

View File

@ -63,6 +63,7 @@ def formsemestre_recapcomplet(
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=True, force_publishing=True,
selected_etudid=None, selected_etudid=None,
visible_col_ids=None,
): ):
"""Page récapitulant les notes d'un semestre. """Page récapitulant les notes d'un semestre.
Grand tableau récapitulatif avec toutes les notes de modules Grand tableau récapitulatif avec toutes les notes de modules
@ -86,7 +87,7 @@ def formsemestre_recapcomplet(
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
abort(404) abort(404)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) 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"} supported_formats = file_formats | {"html", "evals"}
if tabformat not in supported_formats: if tabformat not in supported_formats:
raise ScoValueError(f"Format non supporté: {tabformat}") raise ScoValueError(f"Format non supporté: {tabformat}")
@ -94,6 +95,7 @@ def formsemestre_recapcomplet(
mode_jury = int(mode_jury) mode_jury = int(mode_jury)
xml_with_decisions = int(xml_with_decisions) xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing) force_publishing = int(force_publishing)
visible_col_ids = visible_col_ids.split(",") if visible_col_ids else None
filename = scu.sanitize_filename( filename = scu.sanitize_filename(
f"""{'jury' if mode_jury else 'recap' f"""{'jury' if mode_jury else 'recap'
}{'-evals' if tabformat == 'xlsall' else '' }{'-evals' if tabformat == 'xlsall' else ''
@ -107,6 +109,7 @@ def formsemestre_recapcomplet(
filename=filename, filename=filename,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
visible_col_ids=visible_col_ids,
) )
table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html( table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
@ -124,8 +127,9 @@ def formsemestre_recapcomplet(
] ]
if len(formsemestre.inscriptions) > 0: if len(formsemestre.inscriptions) > 0:
H.append( 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" name="formsemestre_id" value="{formsemestre_id}"></input>
<input type="hidden" id="visible_col_ids" name="visible_col_ids" value=""></input>
""" """
) )
if mode_jury: if mode_jury:
@ -133,13 +137,14 @@ def formsemestre_recapcomplet(
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>' f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
) )
H.append( 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 ( for fmt, label in (
("html", "Tableau"), ("html", "Tableau"),
("evals", "Avec toutes les évaluations"), ("evals", "Avec toutes les évaluations"),
("xlsx", "Excel (non formaté)"), ("xlsx", "Excel (non formaté)"),
("xlsall", "Excel avec évaluations"), ("xlsall", "Excel avec évaluations"),
("xlsvisible", "Excel avec colonnes telles affichées"),
("json", "Bulletins JSON"), ("json", "Bulletins JSON"),
): ):
if fmt == tabformat: 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_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=True, force_publishing=True,
visible_col_ids=None,
): ):
"""Calcule et renvoie le tableau récapitulatif.""" """Calcule et renvoie le tableau récapitulatif."""
if tabformat.startswith("xls"): if tabformat.startswith("xls"):
include_evaluations = tabformat == "xlsall" include_evaluations = tabformat == "xlsall"
visible_col_ids = visible_col_ids if tabformat == "xlsvisible" else None
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
data, filename = gen_formsemestre_recapcomplet_excel( data, filename = gen_formsemestre_recapcomplet_excel(
res, res,
mode_jury=mode_jury, mode_jury=mode_jury,
include_evaluations=include_evaluations, include_evaluations=include_evaluations,
filename=filename, filename=filename,
visible_col_ids=visible_col_ids,
) )
mime, suffix = scu.get_mime_suffix("xlsx") mime, suffix = scu.get_mime_suffix("xlsx")
return scu.send_file(data, filename=filename, mime=mime, suffix=suffix) return scu.send_file(data, filename=filename, mime=mime, suffix=suffix)
@ -537,11 +545,13 @@ def gen_formsemestre_recapcomplet_excel(
mode_jury: bool = False, mode_jury: bool = False,
include_evaluations=False, include_evaluations=False,
filename: str = "", filename: str = "",
visible_col_ids: list[str] | None = None,
) -> tuple: ) -> tuple:
"""Génère le tableau recap ou jury en excel (xlsx). """Génère le tableau recap ou jury en excel (xlsx).
Utilisé pour menu (export excel), archives et autres besoins particuliers (API). 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, Attention: le tableau exporté depuis la page html est celui généré en js par DataTables,
et non celui-ci. 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. # En excel, ajoute les adresses mail, si on a le droit de les voir.
table = _gen_formsemestre_recapcomplet_table( table = _gen_formsemestre_recapcomplet_table(
@ -552,5 +562,9 @@ def gen_formsemestre_recapcomplet_excel(
convert_values=False, convert_values=False,
filename=filename, filename=filename,
) )
if visible_col_ids is not None:
return table.excel(), filename # 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

View File

@ -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();
}

View File

@ -90,8 +90,8 @@ class Table(Element):
"ordered list of column groups names" "ordered list of column groups names"
self.group_titles = {} self.group_titles = {}
"title (in header top row) for the group" "title (in header top row) for the group"
self.head = [] self.head: list["Row"] = []
self.foot = [] self.foot: list["Row"] = []
self.column_group = {} self.column_group = {}
"the group of the column: { col_id : group }" "the group of the column: { col_id : group }"
self.column_classes: defaultdict[str, set[str]] = defaultdict(set) self.column_classes: defaultdict[str, set[str]] = defaultdict(set)
@ -281,6 +281,7 @@ class Table(Element):
col_id, col_id,
None, None,
title, title,
attrs={"data-col_id": col_id},
classes=classes, classes=classes,
group=self.column_group.get(col_id), group=self.column_group.get(col_id),
raw_content=raw_title or title, 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 foot_cell = self.foot_title_row.cells[col_id] if self.foot_title_row else None
return head_cell, foot_cell return head_cell, foot_cell
def excel(self, wb: Workbook = None): def excel(self, wb: Workbook = None, col_ids=None):
"""Simple Excel representation of the table.""" """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() self._prepare()
if wb is None: if wb is None:
sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) 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() style_base = self.xls_style_base or sco_excel.excel_make_style()
for row in self.head: 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: 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: 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: if self.caption:
sheet.append_blank_row() # empty line sheet.append_blank_row() # empty line
@ -325,9 +328,10 @@ class Table(Element):
sheet.append_single_cell_row(self.origin, style_base) sheet.append_single_cell_row(self.origin, style_base)
# Largeurs des colonnes # 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(): for col_id, width in self.xls_columns_width.items():
try: try:
idx = self.column_ids.index(col_id) idx = actual_col_ids.index(col_id)
col = get_column_letter(idx + 1) col = get_column_letter(idx + 1)
sheet.set_column_dimension_width(col, width) sheet.set_column_dimension_width(col, width)
except ValueError: except ValueError:
@ -365,7 +369,7 @@ class Row(Element):
title: str, title: str,
content: str, content: str,
group: str = None, group: str = None,
attrs: list[str] = None, attrs: dict[str, str] = None,
classes: list[str] = None, classes: list[str] = None,
data: dict[str, str] = None, data: dict[str, str] = None,
elt: str = None, elt: str = None,
@ -466,9 +470,15 @@ class Row(Element):
for col_id in self.table.raw_column_ids for col_id in self.table.raw_column_ids
} }
def to_excel(self, sheet, style=None) -> list: def to_excel(self, sheet, style=None, col_ids=None) -> list:
"build excel row for given sheet" """Build excel row for given sheet.
return sheet.make_row(self.to_dict().values(), style=style) 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): class BottomRow(Row):