2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
2023-12-31 23:04:06 +01:00
|
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
#
|
|
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""Géneration de tables aux formats XHTML, PDF, Excel, XML et JSON.
|
|
|
|
|
|
|
|
Les données sont fournies comme une liste de dictionnaires, chaque élément de
|
|
|
|
cette liste décrivant une ligne du tableau.
|
|
|
|
|
|
|
|
Chaque colonne est identifiée par une clé du dictionnaire.
|
|
|
|
|
|
|
|
Voir exemple en fin de ce fichier.
|
|
|
|
|
|
|
|
Les clés commençant par '_' sont réservées. Certaines altèrent le traitement, notamment
|
|
|
|
pour spécifier les styles de mise en forme.
|
|
|
|
Par exemple, la clé '_css_row_class' spécifie le style CSS de la ligne.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import random
|
|
|
|
from collections import OrderedDict
|
2021-07-10 13:55:35 +02:00
|
|
|
from xml.etree import ElementTree
|
2021-01-01 18:40:47 +01:00
|
|
|
import json
|
2024-02-14 21:45:58 +01:00
|
|
|
from typing import Any
|
|
|
|
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
|
|
|
|
2022-07-01 22:29:19 +02:00
|
|
|
from openpyxl.utils import get_column_letter
|
2024-02-14 21:45:58 +01:00
|
|
|
from reportlab.platypus import Paragraph, Spacer
|
|
|
|
from reportlab.platypus import Table, KeepInFrame
|
2021-01-01 18:40:47 +01:00
|
|
|
from reportlab.lib.colors import Color
|
|
|
|
from reportlab.lib import styles
|
2024-02-14 21:45:58 +01:00
|
|
|
from reportlab.lib.units import cm
|
2021-01-01 18:40:47 +01:00
|
|
|
|
2021-06-21 10:17:16 +02:00
|
|
|
from app.scodoc import html_sco_header
|
|
|
|
from app.scodoc import sco_utils as scu
|
|
|
|
from app.scodoc import sco_excel
|
|
|
|
from app.scodoc import sco_pdf
|
2021-07-10 13:55:35 +02:00
|
|
|
from app.scodoc import sco_xml
|
2022-01-13 22:36:40 +01:00
|
|
|
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc.sco_pdf import SU
|
2023-04-04 09:57:54 +02:00
|
|
|
from app import log, ScoDocJSONEncoder
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2024-02-14 21:45:58 +01:00
|
|
|
def mark_paras(items: list[Any], tags: list[str]) -> list[str]:
|
|
|
|
"""Put each string element of items between <tag>...</tag>,
|
2022-03-10 19:36:30 +01:00
|
|
|
for each supplied tag.
|
|
|
|
Leave non string elements untouched.
|
|
|
|
"""
|
2020-09-26 16:19:37 +02:00
|
|
|
for tag in tags:
|
2022-03-10 19:36:30 +01:00
|
|
|
start = "<" + tag + ">"
|
|
|
|
end = "</" + tag.split()[0] + ">"
|
2024-02-14 21:45:58 +01:00
|
|
|
items = [(start + (x or "") + end) if isinstance(x, str) else x for x in items]
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
|
|
|
def add_query_param(url: str, key: str, value: str) -> str:
|
|
|
|
"add parameter key=value to the given URL"
|
|
|
|
# Parse the URL
|
|
|
|
parsed_url = urlparse(url)
|
|
|
|
# Parse the query parameters
|
|
|
|
query_params = parse_qs(parsed_url.query)
|
|
|
|
# Add or update the query parameter
|
|
|
|
query_params[key] = [value]
|
|
|
|
# Encode the query parameters
|
|
|
|
encoded_query_params = urlencode(query_params, doseq=True)
|
|
|
|
# Construct the new URL
|
|
|
|
new_url_parts = parsed_url._replace(query=encoded_query_params)
|
|
|
|
new_url = urlunparse(new_url_parts)
|
|
|
|
return new_url
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2021-07-09 23:31:16 +02:00
|
|
|
class DEFAULT_TABLE_PREFERENCES(object):
|
2021-06-19 23:21:37 +02:00
|
|
|
"""Default preferences for tables created without preferences argument"""
|
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
values = {
|
|
|
|
"SCOLAR_FONT": "Helvetica", # used for PDF, overriden by preferences argument
|
|
|
|
"SCOLAR_FONT_SIZE": 10,
|
|
|
|
"SCOLAR_FONT_SIZE_FOOT": 6,
|
2021-07-25 22:31:59 +02:00
|
|
|
"bul_pdf_with_background": False,
|
2020-09-26 16:19:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
def __getitem__(self, k):
|
|
|
|
return self.values[k]
|
|
|
|
|
|
|
|
|
2023-07-02 12:09:17 +02:00
|
|
|
class GenTable:
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
|
|
|
|
Can be sub-classed to generate fancy formats.
|
|
|
|
"""
|
|
|
|
|
|
|
|
default_css_class = "gt_table stripe cell-border compact hover order-column"
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
rows=[{}], # liste de dict { column_id : value }
|
|
|
|
columns_ids=[], # id des colonnes a afficher, dans l'ordre
|
|
|
|
titles={}, # titres (1ere ligne)
|
|
|
|
bottom_titles={}, # titres derniere ligne (optionnel)
|
|
|
|
caption=None,
|
|
|
|
page_title="", # titre fenetre html
|
|
|
|
pdf_link=True,
|
|
|
|
xls_link=True,
|
|
|
|
xml_link=False,
|
|
|
|
table_id=None, # for html and xml
|
|
|
|
html_class=None, # class de l'element <table> (en plus des classes par defaut,
|
|
|
|
html_class_ignore_default=False, # sauf si html_class_ignore_default est vrai)
|
|
|
|
html_sortable=False,
|
|
|
|
html_highlight_n=2, # une ligne sur 2 de classe "gt_hl"
|
|
|
|
html_col_width=None, # force largeur colonne
|
|
|
|
html_generate_cells=True, # generate empty <td> cells even if not in rows (useless?)
|
|
|
|
html_title="", # avant le tableau en html
|
|
|
|
html_caption=None, # override caption if specified
|
|
|
|
html_header=None,
|
|
|
|
html_next_section="", # html fragment to put after the table
|
|
|
|
html_with_td_classes=False, # put class=column_id in each <td>
|
|
|
|
html_before_table="", # html snippet to put before the <table> in the page
|
|
|
|
html_empty_element="", # replace table when empty
|
2022-04-16 15:34:40 +02:00
|
|
|
html_table_attrs="", # for html
|
2020-09-26 16:19:37 +02:00
|
|
|
base_url=None,
|
|
|
|
origin=None, # string added to excel and xml versions
|
|
|
|
filename="table", # filename, without extension
|
|
|
|
xls_sheet_name="feuille",
|
|
|
|
xls_before_table=[], # liste de cellules a placer avant la table
|
2022-06-30 23:49:39 +02:00
|
|
|
xls_style_base=None, # style excel pour les cellules
|
2022-07-01 22:29:19 +02:00
|
|
|
xls_columns_width=None, # { col_id : largeur en "pixels excel" }
|
2020-09-26 16:19:37 +02:00
|
|
|
pdf_title="", # au dessus du tableau en pdf
|
|
|
|
pdf_table_style=None,
|
|
|
|
pdf_col_widths=None,
|
|
|
|
xml_outer_tag="table",
|
|
|
|
xml_row_tag="row",
|
2021-04-25 10:10:46 +02:00
|
|
|
text_with_titles=False, # CSV with header line
|
2020-09-26 16:19:37 +02:00
|
|
|
text_fields_separator="\t",
|
|
|
|
preferences=None,
|
|
|
|
):
|
|
|
|
self.rows = rows # [ { col_id : value } ]
|
|
|
|
self.columns_ids = columns_ids # ordered list of col_id
|
|
|
|
self.titles = titles # { col_id : title }
|
|
|
|
self.bottom_titles = bottom_titles
|
|
|
|
self.origin = origin
|
|
|
|
self.base_url = base_url
|
|
|
|
self.filename = filename
|
|
|
|
self.caption = caption
|
|
|
|
self.html_header = html_header
|
|
|
|
self.html_before_table = html_before_table
|
|
|
|
self.html_empty_element = html_empty_element
|
2022-04-16 15:34:40 +02:00
|
|
|
self.html_table_attrs = html_table_attrs
|
2020-09-26 16:19:37 +02:00
|
|
|
self.page_title = page_title
|
|
|
|
self.pdf_link = pdf_link
|
|
|
|
self.xls_link = xls_link
|
2022-06-30 23:49:39 +02:00
|
|
|
self.xls_style_base = xls_style_base
|
2022-07-01 22:29:19 +02:00
|
|
|
self.xls_columns_width = xls_columns_width or {}
|
2020-09-26 16:19:37 +02:00
|
|
|
self.xml_link = xml_link
|
|
|
|
# HTML parameters:
|
|
|
|
if not table_id: # random id
|
|
|
|
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
|
|
|
else:
|
|
|
|
self.table_id = table_id
|
|
|
|
self.html_generate_cells = html_generate_cells
|
|
|
|
self.html_title = html_title
|
|
|
|
self.html_caption = html_caption
|
|
|
|
self.html_next_section = html_next_section
|
|
|
|
self.html_with_td_classes = html_with_td_classes
|
|
|
|
if html_class is None:
|
|
|
|
html_class = self.default_css_class
|
|
|
|
if html_class_ignore_default:
|
|
|
|
self.html_class = html_class
|
|
|
|
else:
|
|
|
|
self.html_class = self.default_css_class + " " + html_class
|
|
|
|
|
|
|
|
self.sortable = html_sortable
|
|
|
|
self.html_highlight_n = html_highlight_n
|
|
|
|
self.html_col_width = html_col_width
|
|
|
|
# XLS parameters
|
|
|
|
self.xls_sheet_name = xls_sheet_name
|
|
|
|
self.xls_before_table = xls_before_table
|
|
|
|
# PDF parameters
|
|
|
|
self.pdf_table_style = pdf_table_style
|
|
|
|
self.pdf_col_widths = pdf_col_widths
|
|
|
|
self.pdf_title = pdf_title
|
|
|
|
# XML parameters
|
|
|
|
self.xml_outer_tag = xml_outer_tag
|
|
|
|
self.xml_row_tag = xml_row_tag
|
|
|
|
# TEXT parameters
|
|
|
|
self.text_fields_separator = text_fields_separator
|
2021-04-25 10:10:46 +02:00
|
|
|
self.text_with_titles = text_with_titles
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
if preferences:
|
|
|
|
self.preferences = preferences
|
|
|
|
else:
|
|
|
|
self.preferences = DEFAULT_TABLE_PREFERENCES()
|
|
|
|
|
2021-09-25 10:43:06 +02:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
|
|
|
|
2023-07-02 12:09:17 +02:00
|
|
|
def __len__(self):
|
|
|
|
return len(self.rows)
|
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
def get_nb_cols(self):
|
|
|
|
return len(self.columns_ids)
|
|
|
|
|
|
|
|
def get_nb_rows(self):
|
|
|
|
return len(self.rows)
|
|
|
|
|
|
|
|
def is_empty(self):
|
|
|
|
return len(self.rows) == 0
|
|
|
|
|
|
|
|
def get_data_list(
|
|
|
|
self,
|
|
|
|
with_titles=False,
|
|
|
|
with_lines_titles=True,
|
|
|
|
with_bottom_titles=True,
|
|
|
|
omit_hidden_lines=False,
|
|
|
|
pdf_mode=False, # apply special pdf reportlab processing
|
|
|
|
pdf_style_list=[], # modified: list of platypus table style commands
|
2022-04-06 18:51:01 +02:00
|
|
|
xls_mode=False, # get xls content if available
|
|
|
|
) -> list:
|
2020-09-26 16:19:37 +02:00
|
|
|
"table data as a list of lists (rows)"
|
|
|
|
T = []
|
|
|
|
line_num = 0 # line number in input data
|
|
|
|
out_line_num = 0 # line number in output list
|
|
|
|
if with_titles and self.titles:
|
|
|
|
l = []
|
|
|
|
if with_lines_titles:
|
2021-07-09 13:59:01 +02:00
|
|
|
if "row_title" in self.titles:
|
2020-09-26 16:19:37 +02:00
|
|
|
l = [self.titles["row_title"]]
|
|
|
|
|
|
|
|
T.append(l + [self.titles.get(cid, "") for cid in self.columns_ids])
|
|
|
|
|
|
|
|
for row in self.rows:
|
|
|
|
line_num += 1
|
|
|
|
l = []
|
|
|
|
if with_lines_titles:
|
2021-07-09 13:59:01 +02:00
|
|
|
if "row_title" in row:
|
2020-09-26 16:19:37 +02:00
|
|
|
l = [row["row_title"]]
|
|
|
|
|
|
|
|
if not (omit_hidden_lines and row.get("_hidden", False)):
|
|
|
|
colspan_count = 0
|
|
|
|
col_num = len(l)
|
|
|
|
for cid in self.columns_ids:
|
|
|
|
colspan_count -= 1
|
|
|
|
# if colspan_count > 0:
|
|
|
|
# continue # skip cells after a span
|
2022-03-09 13:59:40 +01:00
|
|
|
if pdf_mode:
|
2022-04-06 18:51:01 +02:00
|
|
|
content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
|
|
|
|
elif xls_mode:
|
|
|
|
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
|
2022-03-09 13:59:40 +01:00
|
|
|
else:
|
2022-04-06 18:51:01 +02:00
|
|
|
content = row.get(cid, "")
|
|
|
|
# Convert None to empty string ""
|
|
|
|
content = "" if content is None else content
|
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
colspan = row.get("_%s_colspan" % cid, 0)
|
|
|
|
if colspan > 1:
|
|
|
|
pdf_style_list.append(
|
|
|
|
(
|
|
|
|
"SPAN",
|
|
|
|
(col_num, out_line_num),
|
|
|
|
(col_num + colspan - 1, out_line_num),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
colspan_count = colspan
|
|
|
|
l.append(content)
|
|
|
|
col_num += 1
|
|
|
|
if pdf_mode:
|
|
|
|
mk = row.get("_pdf_row_markup", []) # a list of tags
|
|
|
|
if mk:
|
|
|
|
l = mark_paras(l, mk)
|
|
|
|
T.append(l)
|
|
|
|
#
|
|
|
|
for cmd in row.get("_pdf_style", []): # relocate line numbers
|
|
|
|
pdf_style_list.append(
|
|
|
|
(
|
|
|
|
cmd[0],
|
|
|
|
(cmd[1][0], cmd[1][1] + out_line_num),
|
|
|
|
(cmd[2][0], cmd[2][1] + out_line_num),
|
|
|
|
)
|
|
|
|
+ cmd[3:]
|
|
|
|
)
|
|
|
|
|
|
|
|
out_line_num += 1
|
|
|
|
if with_bottom_titles and self.bottom_titles:
|
|
|
|
line_num += 1
|
|
|
|
l = []
|
|
|
|
if with_lines_titles:
|
2021-07-09 13:59:01 +02:00
|
|
|
if "row_title" in self.bottom_titles:
|
2020-09-26 16:19:37 +02:00
|
|
|
l = [self.bottom_titles["row_title"]]
|
|
|
|
|
|
|
|
T.append(l + [self.bottom_titles.get(cid, "") for cid in self.columns_ids])
|
|
|
|
return T
|
|
|
|
|
|
|
|
def get_titles_list(self):
|
|
|
|
"list of titles"
|
2021-04-25 10:10:46 +02:00
|
|
|
return [self.titles.get(cid, "") for cid in self.columns_ids]
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-09-21 10:20:19 +02:00
|
|
|
def gen(self, fmt="html", columns_ids=None):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Build representation of the table in the specified format.
|
|
|
|
See make_page() for more sophisticated output.
|
|
|
|
"""
|
2023-09-21 10:20:19 +02:00
|
|
|
if fmt == "html":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.html()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "xls" or fmt == "xlsx":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.excel()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "text" or fmt == "csv":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.text()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "pdf":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.pdf()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "xml":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.xml()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "json":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.json()
|
2023-09-21 10:20:19 +02:00
|
|
|
raise ValueError(f"GenTable: invalid format: {fmt}")
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
|
|
|
|
"row is a dict, returns a string <tr...>...</tr>"
|
|
|
|
if not row:
|
|
|
|
return "<tr></tr>" # empty row
|
|
|
|
|
2021-01-01 18:40:47 +01:00
|
|
|
if self.html_col_width:
|
2020-09-26 16:19:37 +02:00
|
|
|
std = ' style="width:%s;"' % self.html_col_width
|
|
|
|
else:
|
|
|
|
std = ""
|
|
|
|
|
|
|
|
cla = css_classes + " " + row.get("_css_row_class", "")
|
|
|
|
if line_num % self.html_highlight_n:
|
|
|
|
cls = ' class="gt_hl %s"' % cla
|
|
|
|
else:
|
|
|
|
if cla:
|
|
|
|
cls = ' class="%s"' % cla
|
|
|
|
else:
|
|
|
|
cls = ""
|
|
|
|
H = ["<tr%s %s>" % (cls, row.get("_tr_attrs", ""))]
|
|
|
|
# titre ligne
|
2021-07-09 13:59:01 +02:00
|
|
|
if "row_title" in row:
|
2020-09-26 16:19:37 +02:00
|
|
|
content = str(row["row_title"])
|
|
|
|
help = row.get("row_title_help", "")
|
|
|
|
if help:
|
|
|
|
content = '<a class="discretelink" href="" title="%s">%s</a>' % (
|
|
|
|
help,
|
|
|
|
content,
|
|
|
|
)
|
|
|
|
H.append('<th class="gt_linetit">' + content + "</th>")
|
|
|
|
r = []
|
|
|
|
colspan_count = 0
|
|
|
|
for cid in self.columns_ids:
|
|
|
|
if not cid in row and not self.html_generate_cells:
|
|
|
|
continue # skip cell
|
|
|
|
colspan_count -= 1
|
|
|
|
if colspan_count > 0:
|
|
|
|
continue # skip cells after a span
|
|
|
|
content = row.get("_" + str(cid) + "_html", row.get(cid, ""))
|
|
|
|
if content is None:
|
|
|
|
content = ""
|
|
|
|
else:
|
|
|
|
content = str(content)
|
|
|
|
help = row.get("_%s_help" % cid, "")
|
|
|
|
if help:
|
|
|
|
target = row.get("_%s_target" % cid, "#")
|
|
|
|
else:
|
|
|
|
target = row.get("_%s_target" % cid, "")
|
|
|
|
cell_id = row.get("_%s_id" % cid, None)
|
|
|
|
if cell_id:
|
|
|
|
idstr = ' id="%s"' % cell_id
|
|
|
|
else:
|
|
|
|
idstr = ""
|
|
|
|
cell_link_class = row.get("_%s_link_class" % cid, "discretelink")
|
|
|
|
if help or target:
|
|
|
|
content = '<a class="%s" href="%s" title="%s"%s>%s</a>' % (
|
|
|
|
cell_link_class,
|
|
|
|
target,
|
|
|
|
help,
|
|
|
|
idstr,
|
|
|
|
content,
|
|
|
|
)
|
|
|
|
klass = row.get("_%s_class" % cid, "")
|
|
|
|
if self.html_with_td_classes:
|
|
|
|
c = cid
|
|
|
|
else:
|
|
|
|
c = ""
|
|
|
|
if c or klass:
|
|
|
|
klass = ' class="%s"' % (" ".join((klass, c)))
|
|
|
|
else:
|
|
|
|
klass = ""
|
|
|
|
colspan = row.get("_%s_colspan" % cid, 0)
|
|
|
|
if colspan > 1:
|
|
|
|
colspan_txt = ' colspan="%d" ' % colspan
|
|
|
|
colspan_count = colspan
|
|
|
|
else:
|
|
|
|
colspan_txt = ""
|
2022-04-20 22:55:40 +02:00
|
|
|
attrs = row.get("_%s_td_attrs" % cid, "")
|
|
|
|
order = row.get(f"_{cid}_order")
|
|
|
|
if order:
|
|
|
|
attrs += f' data-order="{order}"'
|
2020-09-26 16:19:37 +02:00
|
|
|
r.append(
|
|
|
|
"<%s%s %s%s%s>%s</%s>"
|
|
|
|
% (
|
|
|
|
elem,
|
|
|
|
std,
|
2022-04-20 22:55:40 +02:00
|
|
|
attrs,
|
2020-09-26 16:19:37 +02:00
|
|
|
klass,
|
|
|
|
colspan_txt,
|
|
|
|
content,
|
|
|
|
elem,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
H.append("".join(r) + "</tr>")
|
|
|
|
return "".join(H)
|
|
|
|
|
|
|
|
def html(self):
|
|
|
|
"Simple HTML representation of the table"
|
|
|
|
if self.is_empty() and self.html_empty_element:
|
|
|
|
return self.html_empty_element + "\n" + self.html_next_section
|
|
|
|
hid = ' id="%s"' % self.table_id
|
|
|
|
tablclasses = []
|
|
|
|
if self.html_class:
|
|
|
|
tablclasses.append(self.html_class)
|
|
|
|
if self.sortable:
|
|
|
|
tablclasses.append("sortable")
|
|
|
|
if tablclasses:
|
|
|
|
cls = ' class="%s"' % " ".join(tablclasses)
|
|
|
|
else:
|
|
|
|
cls = ""
|
2022-04-16 15:34:40 +02:00
|
|
|
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
line_num = 0
|
|
|
|
# thead
|
|
|
|
H.append("<thead>")
|
|
|
|
if self.titles:
|
|
|
|
H.append(
|
|
|
|
self._gen_html_row(
|
|
|
|
self.titles, line_num, elem="th", css_classes="gt_firstrow"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
# autres lignes à placer dans la tête:
|
|
|
|
for row in self.rows:
|
|
|
|
if row.get("_table_part") == "head":
|
|
|
|
line_num += 1
|
|
|
|
H.append(self._gen_html_row(row, line_num)) # uses td elements
|
|
|
|
H.append("</thead>")
|
|
|
|
|
|
|
|
H.append("<tbody>")
|
|
|
|
for row in self.rows:
|
|
|
|
if row.get("_table_part", "body") == "body":
|
|
|
|
line_num += 1
|
|
|
|
H.append(self._gen_html_row(row, line_num))
|
|
|
|
H.append("</tbody>")
|
|
|
|
|
|
|
|
H.append("<tfoot>")
|
|
|
|
for row in self.rows:
|
|
|
|
if row.get("_table_part") == "foot":
|
|
|
|
line_num += 1
|
|
|
|
H.append(self._gen_html_row(row, line_num))
|
|
|
|
if self.bottom_titles:
|
|
|
|
H.append(
|
|
|
|
self._gen_html_row(
|
|
|
|
self.bottom_titles,
|
|
|
|
line_num + 1,
|
|
|
|
elem="th",
|
|
|
|
css_classes="gt_lastrow sortbottom",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
H.append("</tfoot>")
|
|
|
|
|
|
|
|
H.append("</table>")
|
|
|
|
|
|
|
|
caption = self.html_caption or self.caption
|
|
|
|
if caption or self.base_url:
|
|
|
|
H.append('<p class="gt_caption">')
|
|
|
|
if caption:
|
|
|
|
H.append(caption)
|
|
|
|
if self.base_url:
|
2021-08-22 19:30:58 +02:00
|
|
|
H.append('<span class="gt_export_icons">')
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.xls_link:
|
|
|
|
H.append(
|
2024-02-14 21:45:58 +01:00
|
|
|
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
|
|
|
|
}">{scu.ICON_XLS}</a>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
if self.xls_link and self.pdf_link:
|
2021-08-22 19:30:58 +02:00
|
|
|
H.append(" ")
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.pdf_link:
|
|
|
|
H.append(
|
2024-02-14 21:45:58 +01:00
|
|
|
f""" <a href="{add_query_param(self.base_url, "fmt", "pdf")
|
|
|
|
}">{scu.ICON_PDF}</a>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-08-22 19:30:58 +02:00
|
|
|
H.append("</span>")
|
2020-09-26 16:19:37 +02:00
|
|
|
H.append("</p>")
|
|
|
|
|
|
|
|
H.append(self.html_next_section)
|
|
|
|
return "\n".join(H)
|
|
|
|
|
|
|
|
def excel(self, wb=None):
|
2021-08-11 11:40:28 +02:00
|
|
|
"""Simple Excel representation of the table"""
|
2021-09-26 23:27:54 +02:00
|
|
|
if wb is None:
|
2022-04-06 18:51:01 +02:00
|
|
|
sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
|
2021-09-26 23:27:54 +02:00
|
|
|
else:
|
2022-04-06 18:51:01 +02:00
|
|
|
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
|
|
|
sheet.rows += self.xls_before_table
|
2021-08-11 11:40:28 +02:00
|
|
|
style_bold = sco_excel.excel_make_style(bold=True)
|
2022-06-30 23:49:39 +02:00
|
|
|
style_base = self.xls_style_base or sco_excel.excel_make_style()
|
|
|
|
|
2022-04-06 18:51:01 +02:00
|
|
|
sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
|
|
|
|
for line in self.get_data_list(xls_mode=True):
|
|
|
|
sheet.append_row(sheet.make_row(line, style_base))
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.caption:
|
2022-04-06 18:51:01 +02:00
|
|
|
sheet.append_blank_row() # empty line
|
|
|
|
sheet.append_single_cell_row(self.caption, style_base)
|
2020-09-26 16:19:37 +02:00
|
|
|
if self.origin:
|
2022-04-06 18:51:01 +02:00
|
|
|
sheet.append_blank_row() # empty line
|
|
|
|
sheet.append_single_cell_row(self.origin, style_base)
|
2022-07-01 22:29:19 +02:00
|
|
|
# Largeurs des colonnes
|
|
|
|
columns_ids = list(self.columns_ids)
|
|
|
|
for col_id, width in self.xls_columns_width.items():
|
|
|
|
try:
|
|
|
|
idx = columns_ids.index(col_id)
|
|
|
|
col = get_column_letter(idx + 1)
|
|
|
|
sheet.set_column_dimension_width(col, width)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
2021-08-11 11:40:28 +02:00
|
|
|
if wb is None:
|
2022-04-06 18:51:01 +02:00
|
|
|
return sheet.generate()
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def text(self):
|
|
|
|
"raw text representation of the table"
|
2021-04-25 10:10:46 +02:00
|
|
|
if self.text_with_titles:
|
|
|
|
headline = [self.get_titles_list()]
|
|
|
|
else:
|
|
|
|
headline = []
|
2020-09-26 16:19:37 +02:00
|
|
|
return "\n".join(
|
|
|
|
[
|
2021-11-23 15:10:34 +01:00
|
|
|
self.text_fields_separator.join([str(x) for x in line])
|
2021-04-25 10:10:46 +02:00
|
|
|
for line in headline + self.get_data_list()
|
2020-09-26 16:19:37 +02:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|
2023-09-01 14:38:41 +02:00
|
|
|
def pdf(self) -> list:
|
|
|
|
"PDF representation: returns a list of ReportLab's platypus objects"
|
2020-09-26 16:19:37 +02:00
|
|
|
r = []
|
|
|
|
try:
|
2021-01-01 18:40:47 +01:00
|
|
|
sco_pdf.PDFLOCK.acquire()
|
2020-09-26 16:19:37 +02:00
|
|
|
r = self._pdf()
|
|
|
|
finally:
|
2021-01-01 18:40:47 +01:00
|
|
|
sco_pdf.PDFLOCK.release()
|
2020-09-26 16:19:37 +02:00
|
|
|
return r
|
|
|
|
|
2023-09-01 14:38:41 +02:00
|
|
|
def _pdf(self) -> list:
|
2020-09-26 16:19:37 +02:00
|
|
|
"""PDF representation: returns a list of ReportLab's platypus objects
|
|
|
|
(notably a Table instance)
|
|
|
|
"""
|
2023-09-01 14:38:41 +02:00
|
|
|
LINEWIDTH = 0.5
|
2020-09-26 16:19:37 +02:00
|
|
|
if not self.pdf_table_style:
|
|
|
|
self.pdf_table_style = [
|
|
|
|
("FONTNAME", (0, 0), (-1, 0), self.preferences["SCOLAR_FONT"]),
|
|
|
|
("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
|
|
|
|
("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
|
|
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
|
|
]
|
|
|
|
nb_cols = len(self.columns_ids)
|
2021-07-09 13:59:01 +02:00
|
|
|
if self.rows and "row_title" in self.rows[0]:
|
2020-09-26 16:19:37 +02:00
|
|
|
nb_cols += 1
|
|
|
|
if not self.pdf_col_widths:
|
|
|
|
self.pdf_col_widths = (None,) * nb_cols
|
|
|
|
#
|
|
|
|
CellStyle = styles.ParagraphStyle({})
|
|
|
|
CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
|
|
|
|
CellStyle.fontName = self.preferences["SCOLAR_FONT"]
|
|
|
|
CellStyle.leading = 1.0 * self.preferences["SCOLAR_FONT_SIZE"] # vertical space
|
|
|
|
#
|
2021-01-01 18:40:47 +01:00
|
|
|
# titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
|
2020-09-26 16:19:37 +02:00
|
|
|
pdf_style_list = []
|
2022-01-13 21:13:09 +01:00
|
|
|
data_list = self.get_data_list(
|
|
|
|
pdf_mode=True,
|
|
|
|
pdf_style_list=pdf_style_list,
|
|
|
|
with_titles=True,
|
|
|
|
omit_hidden_lines=True,
|
|
|
|
)
|
|
|
|
try:
|
2022-03-09 13:59:40 +01:00
|
|
|
Pt = []
|
|
|
|
for line in data_list:
|
|
|
|
Pt.append(
|
|
|
|
[
|
2024-02-14 21:45:58 +01:00
|
|
|
(
|
|
|
|
Paragraph(SU(str(x)), CellStyle)
|
|
|
|
if (not isinstance(x, Paragraph))
|
|
|
|
else x
|
|
|
|
)
|
2022-03-09 13:59:40 +01:00
|
|
|
for x in line
|
|
|
|
]
|
|
|
|
)
|
2022-01-13 21:13:09 +01:00
|
|
|
except ValueError as exc:
|
2022-01-13 22:36:40 +01:00
|
|
|
raise ScoPDFFormatError(str(exc)) from exc
|
2020-09-26 16:19:37 +02:00
|
|
|
pdf_style_list += self.pdf_table_style
|
|
|
|
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
|
|
|
|
|
|
|
|
objects = []
|
|
|
|
StyleSheet = styles.getSampleStyleSheet()
|
|
|
|
if self.pdf_title:
|
|
|
|
objects.append(Paragraph(SU(self.pdf_title), StyleSheet["Heading3"]))
|
|
|
|
if self.caption:
|
|
|
|
objects.append(Paragraph(SU(self.caption), StyleSheet["Normal"]))
|
|
|
|
objects.append(Spacer(0, 0.4 * cm))
|
|
|
|
objects.append(T)
|
|
|
|
|
|
|
|
return objects
|
|
|
|
|
|
|
|
def xml(self):
|
|
|
|
"""XML representation of the table.
|
|
|
|
The schema is very simple:
|
|
|
|
<table origin="" id="" caption="">
|
|
|
|
<row title="">
|
|
|
|
<column_id value=""/>
|
|
|
|
</row>
|
|
|
|
</table>
|
|
|
|
The tag names <table> and <row> can be changed using
|
|
|
|
xml_outer_tag and xml_row_tag
|
|
|
|
"""
|
2021-07-10 13:55:35 +02:00
|
|
|
doc = ElementTree.Element(
|
|
|
|
self.xml_outer_tag,
|
2021-09-24 00:33:18 +02:00
|
|
|
id=str(self.table_id),
|
2021-07-10 13:55:35 +02:00
|
|
|
origin=self.origin or "",
|
|
|
|
caption=self.caption or "",
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
for row in self.rows:
|
2021-07-10 13:55:35 +02:00
|
|
|
x_row = ElementTree.Element(self.xml_row_tag)
|
2020-09-26 16:19:37 +02:00
|
|
|
row_title = row.get("row_title", "")
|
|
|
|
if row_title:
|
2021-07-10 13:55:35 +02:00
|
|
|
x_row.set("title", row_title)
|
|
|
|
doc.append(x_row)
|
2020-09-26 16:19:37 +02:00
|
|
|
for cid in self.columns_ids:
|
|
|
|
v = row.get(cid, "")
|
|
|
|
if v is None:
|
|
|
|
v = ""
|
2021-09-24 00:33:18 +02:00
|
|
|
x_cell = ElementTree.Element(str(cid), value=str(v))
|
2021-07-10 13:55:35 +02:00
|
|
|
x_row.append(x_cell)
|
2021-07-12 23:34:18 +02:00
|
|
|
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def json(self):
|
2020-10-21 22:56:25 +02:00
|
|
|
"""JSON representation of the table."""
|
2020-09-26 16:19:37 +02:00
|
|
|
d = []
|
|
|
|
for row in self.rows:
|
|
|
|
r = {}
|
|
|
|
for cid in self.columns_ids:
|
|
|
|
v = row.get(cid, None)
|
2021-08-10 13:44:30 +02:00
|
|
|
# if v != None:
|
|
|
|
# v = str(v)
|
2020-09-26 16:19:37 +02:00
|
|
|
r[cid] = v
|
|
|
|
d.append(r)
|
2023-04-04 09:57:54 +02:00
|
|
|
return json.dumps(d, cls=ScoDocJSONEncoder)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
def make_page(
|
|
|
|
self,
|
|
|
|
title="",
|
2023-09-21 10:20:19 +02:00
|
|
|
fmt="html",
|
2020-09-26 16:19:37 +02:00
|
|
|
page_title="",
|
|
|
|
filename=None,
|
2024-03-31 23:04:54 +02:00
|
|
|
cssstyles=[],
|
2020-09-26 16:19:37 +02:00
|
|
|
javascripts=[],
|
|
|
|
with_html_headers=True,
|
|
|
|
publish=True,
|
|
|
|
init_qtip=False,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Build page at given format
|
|
|
|
This is a simple page with only a title and the table.
|
|
|
|
If not publish, does not set response header
|
|
|
|
"""
|
|
|
|
if not filename:
|
|
|
|
filename = self.filename
|
|
|
|
page_title = page_title or self.page_title
|
|
|
|
html_title = self.html_title or title
|
2023-09-21 10:20:19 +02:00
|
|
|
if fmt == "html":
|
2020-09-26 16:19:37 +02:00
|
|
|
H = []
|
|
|
|
if with_html_headers:
|
|
|
|
H.append(
|
|
|
|
self.html_header
|
2021-06-17 00:08:37 +02:00
|
|
|
or html_sco_header.sco_header(
|
2024-03-31 23:04:54 +02:00
|
|
|
cssstyles=cssstyles,
|
2020-09-26 16:19:37 +02:00
|
|
|
page_title=page_title,
|
|
|
|
javascripts=javascripts,
|
|
|
|
init_qtip=init_qtip,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if html_title:
|
|
|
|
H.append(html_title)
|
|
|
|
H.append(self.html())
|
|
|
|
if with_html_headers:
|
2021-07-29 10:19:00 +02:00
|
|
|
H.append(html_sco_header.sco_footer())
|
2020-09-26 16:19:37 +02:00
|
|
|
return "\n".join(H)
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "pdf":
|
2021-09-17 05:03:34 +02:00
|
|
|
pdf_objs = self.pdf()
|
|
|
|
pdf_doc = sco_pdf.pdf_basic_page(
|
|
|
|
pdf_objs, title=title, preferences=self.preferences
|
2020-10-21 22:56:25 +02:00
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
if publish:
|
2021-09-17 05:03:34 +02:00
|
|
|
return scu.send_file(
|
|
|
|
pdf_doc,
|
|
|
|
filename,
|
|
|
|
suffix=".pdf",
|
|
|
|
mime=scu.PDF_MIMETYPE,
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2021-09-17 05:03:34 +02:00
|
|
|
return pdf_doc
|
2024-03-31 23:04:54 +02:00
|
|
|
elif fmt in ("xls", "xlsx"): # dans les 2 cas retourne du xlsx
|
2020-09-26 16:19:37 +02:00
|
|
|
xls = self.excel()
|
|
|
|
if publish:
|
2021-09-17 05:03:34 +02:00
|
|
|
return scu.send_file(
|
|
|
|
xls,
|
|
|
|
filename,
|
|
|
|
suffix=scu.XLSX_SUFFIX,
|
|
|
|
mime=scu.XLSX_MIMETYPE,
|
|
|
|
)
|
2024-03-31 23:04:54 +02:00
|
|
|
return xls
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "text":
|
2020-09-26 16:19:37 +02:00
|
|
|
return self.text()
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "csv":
|
2021-09-17 05:03:34 +02:00
|
|
|
return scu.send_file(
|
|
|
|
self.text(),
|
|
|
|
filename,
|
|
|
|
suffix=".csv",
|
|
|
|
mime=scu.CSV_MIMETYPE,
|
|
|
|
attached=True,
|
|
|
|
)
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "xml":
|
2020-09-26 16:19:37 +02:00
|
|
|
xml = self.xml()
|
2021-09-13 07:16:37 +02:00
|
|
|
if publish:
|
2021-09-17 05:03:34 +02:00
|
|
|
return scu.send_file(
|
2021-09-21 22:19:08 +02:00
|
|
|
xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE
|
2021-09-17 05:03:34 +02:00
|
|
|
)
|
|
|
|
return xml
|
2023-09-21 10:20:19 +02:00
|
|
|
elif fmt == "json":
|
2020-09-26 16:19:37 +02:00
|
|
|
js = self.json()
|
2021-09-13 07:16:37 +02:00
|
|
|
if publish:
|
2021-09-17 05:03:34 +02:00
|
|
|
return scu.send_file(
|
2021-09-21 22:19:08 +02:00
|
|
|
js, filename, suffix=".json", mime=scu.JSON_MIMETYPE
|
2021-09-17 05:03:34 +02:00
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
return js
|
|
|
|
else:
|
2023-09-21 10:20:19 +02:00
|
|
|
log(f"make_page: format={fmt}")
|
2020-09-26 16:19:37 +02:00
|
|
|
raise ValueError("_make_page: invalid format")
|
|
|
|
|
|
|
|
|
|
|
|
# -----
|
2021-07-09 23:31:16 +02:00
|
|
|
class SeqGenTable(object):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Sequence de GenTable: permet de générer un classeur excel avec un tab par table.
|
|
|
|
L'ordre des tabs est conservé (1er tab == 1ere table ajoutée)
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.genTables = OrderedDict()
|
|
|
|
|
|
|
|
def add_genTable(self, name, gentable):
|
|
|
|
self.genTables[name] = gentable
|
|
|
|
|
|
|
|
def get_genTable(self, name):
|
|
|
|
return self.genTables.get(name)
|
|
|
|
|
|
|
|
def excel(self):
|
|
|
|
"""Export des genTables dans un unique fichier excel avec plusieurs feuilles tagguées"""
|
2021-08-23 17:47:49 +02:00
|
|
|
book = sco_excel.ScoExcelBook() # pylint: disable=no-member
|
2023-04-04 09:57:54 +02:00
|
|
|
for _, gt in self.genTables.items():
|
2020-09-26 16:19:37 +02:00
|
|
|
gt.excel(wb=book) # Ecrit dans un fichier excel
|
2021-08-23 17:47:49 +02:00
|
|
|
return book.generate()
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
# ----- Exemple d'utilisation minimal.
|
|
|
|
if __name__ == "__main__":
|
2023-04-16 05:32:21 +02:00
|
|
|
table = GenTable(
|
2021-07-25 22:31:59 +02:00
|
|
|
rows=[{"nom": "Hélène", "age": 26}, {"nom": "Titi&çà§", "age": 21}],
|
2020-09-26 16:19:37 +02:00
|
|
|
columns_ids=("nom", "age"),
|
|
|
|
)
|
|
|
|
print("--- HTML:")
|
2023-09-21 10:20:19 +02:00
|
|
|
print(table.gen(fmt="html"))
|
2020-09-26 16:19:37 +02:00
|
|
|
print("\n--- XML:")
|
2023-09-21 10:20:19 +02:00
|
|
|
print(table.gen(fmt="xml"))
|
2020-09-26 16:19:37 +02:00
|
|
|
print("\n--- JSON:")
|
2023-09-21 10:20:19 +02:00
|
|
|
print(table.gen(fmt="json"))
|
2021-07-25 22:31:59 +02:00
|
|
|
# Test pdf:
|
|
|
|
import io
|
2023-09-10 21:16:31 +02:00
|
|
|
from app.scodoc import sco_preferences
|
2021-07-25 22:31:59 +02:00
|
|
|
|
2021-07-29 10:19:00 +02:00
|
|
|
preferences = sco_preferences.SemPreferences()
|
2023-04-16 05:32:21 +02:00
|
|
|
table.preferences = preferences
|
2023-09-21 10:20:19 +02:00
|
|
|
objects = table.gen(fmt="pdf")
|
2021-07-25 22:31:59 +02:00
|
|
|
objects = [KeepInFrame(0, 0, objects, mode="shrink")]
|
|
|
|
doc = io.BytesIO()
|
|
|
|
document = sco_pdf.BaseDocTemplate(doc)
|
|
|
|
document.addPageTemplates(
|
2022-03-12 09:40:48 +01:00
|
|
|
sco_pdf.ScoDocPageTemplate(
|
2021-07-25 22:31:59 +02:00
|
|
|
document,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
document.build(objects)
|
|
|
|
data = doc.getvalue()
|
2021-10-11 22:22:42 +02:00
|
|
|
with open("/tmp/gen_table.pdf", "wb") as f:
|
|
|
|
f.write(data)
|
2023-09-21 10:20:19 +02:00
|
|
|
p = table.make_page(fmt="pdf")
|
2021-10-11 22:22:42 +02:00
|
|
|
with open("toto.pdf", "wb") as f:
|
|
|
|
f.write(p)
|