From ecb3748c85e5defa09ae2240f755c36abb003ebf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Feb 2023 10:58:36 +0100 Subject: [PATCH] Tableau recap: export xls. (et abandon de l'export CSV). --- app/scodoc/sco_archives.py | 4 +- app/scodoc/sco_excel.py | 6 +-- app/scodoc/sco_recapcomplet.py | 25 +++-------- app/tables/table_builder.py | 78 ++++++++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 38 deletions(-) diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index e48d7e92..c6694e99 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -339,9 +339,7 @@ def do_formsemestre_archive( etudids = [m["etudid"] for m in groups_infos.members] # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) - data, _ = gen_formsemestre_recapcomplet_excel( - formsemestre, res, include_evaluations=True, format="xls" - ) + data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) if data: PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index dc875ca9..553579ea 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -194,12 +194,12 @@ class ScoExcelSheet: * pour finir appel de la méthode de génération """ - def __init__(self, sheet_name="feuille", default_style=None, wb=None): + def __init__(self, sheet_name="feuille", default_style=None, wb: Workbook = None): """Création de la feuille. sheet_name -- le nom de la feuille default_style -- le style par défaut des cellules ws - -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet - créée par le workbook propriétaire un workbook est crée et associé à cette feuille. + -- None si la feuille est autonome (dans ce cas elle crée son propre wb), sinon c'est la worksheet + créée par le workbook propriétaire un workbook est créé et associé à cette feuille. """ # Le nom de la feuille ne peut faire plus de 31 caractères. # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 458aca12..7343ceb2 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -225,16 +225,14 @@ def _do_formsemestre_recapcomplet( selected_etudid=selected_etudid, ) return data - elif format.startswith("xls") or format == "csv": + elif format.startswith("xls"): res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - include_evaluations = format in {"xlsall", "csv "} + include_evaluations = format in {"xlsall", "csv "} # csv not supported anymore if format != "csv": format = "xlsx" data, filename = gen_formsemestre_recapcomplet_excel( - formsemestre, res, include_evaluations=include_evaluations, - format=format, filename=filename, ) return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format)) @@ -446,33 +444,22 @@ def _gen_formsemestre_recapcomplet_html( def gen_formsemestre_recapcomplet_excel( - formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False, filename: str = "", - format="xls", ) -> tuple: - """Génère le tableau recap en excel (xlsx) ou CSV. + """Génère le tableau recap en excel (xlsx). Utilisé pour 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. """ - suffix = scu.CSV_SUFFIX if format == "csv" else scu.XLSX_SUFFIX - filename += suffix + filename += scu.XLSX_SUFFIX - # XXX TODO A ADAPTER XXX !!! !!! table = TableRecap( res, convert_values=False, include_evaluations=include_evaluations, - preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id), + # preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id), ) - # tab = GenTable( - # columns_ids=column_ids, - # titles=titles, - # rows=rows + footer_rows, - # preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id), - # ) - - return table.gen(format=format), filename + return table.excel(), filename diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py index 19e14ea1..50a61f55 100644 --- a/app/tables/table_builder.py +++ b/app/tables/table_builder.py @@ -8,8 +8,15 @@ """ from collections import defaultdict +from openpyxl import Workbook +from openpyxl.utils import get_column_letter + +from app.scodoc import sco_excel + class Element: + "Element de base pour les tables" + def __init__( self, elt: str, @@ -49,18 +56,6 @@ class Element: class Table(Element): """Construction d'une table de résultats - table = Table() - row = table.new_row(id="xxx", category="yyy") - row.new_cell( col_id, title, content [,classes] [, idx], [group], [keys:dict={}] ) - - rows = table.get_rows([category="yyy"]) - table.sort_rows(key [, reverse]) - table.set_titles(titles) - table.update_titles(titles) - - table.set_column_groups(groups: list[str]) - table.insert_group(group:str, [after=str], [before=str]) - Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion On fixe l'ordre des groupes par ordre d'insertion ou par insert_group ou par set_column_groups. @@ -74,6 +69,12 @@ class Table(Element): attrs: dict[str, str] = None, data: dict = None, row_class=None, + xls_sheet_name="feuille", + xls_before_table=[], # liste de cellules a placer avant la table + xls_style_base=None, # style excel pour les cellules + xls_columns_width=None, # { col_id : largeur en "pixels excel" } + caption="", + origin="", ): super().__init__("table", classes=classes, attrs=attrs, data=data) self.row_class = row_class or Row @@ -103,6 +104,14 @@ class Table(Element): self, "title_foot", cell_elt="th", classes=["titles"] ) self.empty_cell = Cell.empty() + # Excel (xls) spécifique: + self.xls_before_table = xls_before_table + self.xls_columns_width = xls_columns_width or {} + self.xls_sheet_name = xls_sheet_name + self.xls_style_base = xls_style_base + # + self.caption = caption + self.origin = origin def _prepare(self): """Prepare the table before generation: @@ -130,7 +139,7 @@ class Table(Element): self.selected_row_id = row_id def to_list(self) -> list[dict]: - """as a list, each row is a dict""" + """as a list, each row is a dict (sans les lignes d'en-tête ni de pied de table)""" self._prepare() return [row.to_dict() for row in self.rows] @@ -265,6 +274,45 @@ class Table(Element): return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id] + def excel(self, wb: Workbook = None): + """Simple Excel representation of the table.""" + self._prepare() + if wb is None: + sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) + else: + sheet = wb.create_sheet(sheet_name=self.xls_sheet_name) + sheet.rows += self.xls_before_table + style_bold = sco_excel.excel_make_style(bold=True) + 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)) + + for row in self.rows: + sheet.append_row(row.to_excel(sheet, style=style_base)) + + for row in self.foot: + sheet.append_row(row.to_excel(sheet, style=style_base)) + + if self.caption: + sheet.append_blank_row() # empty line + sheet.append_single_cell_row(self.caption, style_base) + if self.origin: + sheet.append_blank_row() # empty line + sheet.append_single_cell_row(self.origin, style_base) + + # Largeurs des colonnes + for col_id, width in self.xls_columns_width.items(): + try: + idx = self.column_ids.index(col_id) + col = get_column_letter(idx + 1) + sheet.set_column_dimension_width(col, width) + except ValueError: + pass + + if wb is None: + return sheet.generate() + class Row(Element): """A row.""" @@ -371,6 +419,10 @@ class Row(Element): for col_id in self.table.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) + class BottomRow(Row): """Une ligne spéciale pour le pied de table