diff --git a/.gitignore b/.gitignore
index 55251cc..46ece7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ reports/
venv/
# excludes
.python-version
+*.bak
+nohup.out
diff --git a/src/iosanita/contenttypes/browser/export_view.py b/src/iosanita/contenttypes/browser/export_view.py
index 78d2b84..993f8c0 100644
--- a/src/iosanita/contenttypes/browser/export_view.py
+++ b/src/iosanita/contenttypes/browser/export_view.py
@@ -2,14 +2,18 @@
from io import BytesIO
from io import StringIO
from iosanita.contenttypes import _
+from PIL import Image
from plone import api
+from plone.memoize import forever
from Products.Five.browser import BrowserView
from weasyprint import HTML
-from zExceptions import BadRequest
+from zExceptions import NotFound
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse
+import base64
import csv
+import imghdr
import importlib.resources
import logging
import re
@@ -21,11 +25,58 @@
fontools_logger.setLevel(logging.WARNING)
-CONTENT_TYPES_MAPPING = {
- "csv": "text/comma-separated-values",
- "pdf": "application/pdf",
- "html": "text/html",
-}
+@forever.memoize
+def image_to_html(input_string):
+ """
+ Convert image data to a base64 string formatted for HTML.
+
+ Args:
+ - input_string: The string containing the filename and base64 encoded image data.
+
+ Returns:
+ - HTML.
+ """
+
+ if not input_string:
+ return ""
+
+ # Split the input string to extract the filename and base64 data
+ parts = input_string.split(";")
+ datab64 = parts[1].split(":")[1]
+
+ # Decode the image data from base64
+ image_data = base64.b64decode(datab64)
+
+ if image_data[:5] == b"'
+ # XXX: se non si va decode/encode il b64 non risulta corretto (!)
+ # return f''
+ # weasyprint gli svg non li gestisce comunque correttamente
+ return None
+
+ # Guess the image format
+ image_format = imghdr.what(None, image_data)
+
+ if not image_format:
+ # raise ValueError("Unable to determine image format")
+ logger.warning("site logo, unable to determine image format")
+ return ""
+
+ # Open the image from the decoded data
+ img = Image.open(BytesIO(image_data))
+
+ # Create a buffer to hold the image data
+ buffered = BytesIO()
+ img.save(buffered, format=image_format)
+
+ # Encode the image data to base64
+ img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
+
+ # Format the base64 string for HTML
+ return f'
'
class IExportViewTraverser(IPublishTraverse):
@@ -50,6 +101,8 @@ class ExportViewDownload(BrowserView):
"""
+ with_footer = True
+
def __init__(self, context, request):
super().__init__(context, request)
self.export_type = "csv"
@@ -65,7 +118,7 @@ def publishTraverse(self, request, name):
def __call__(self):
""" """
if self.export_type not in ["csv", "pdf", "html"]:
- raise BadRequest(
+ raise NotFound(
api.portal.translate(
_(
"invalid_export_type",
@@ -74,18 +127,21 @@ def __call__(self):
)
)
)
- self.set_headers()
data = self.get_data()
- if not data:
- return ""
- resp_data = ""
if self.export_type == "csv":
- resp_data = self.get_csv(data)
+ # default per locales di riferimento (perr l'encoding, al momento, lasciamo
+ # il generico utf-8 con BOM che potrebbe funzionare per tutti,
+ # MS Excel incluso)
+ lang = api.portal.get_current_language(self.context)
+ if lang == "it":
+ sep = ";"
+ else:
+ sep = ","
+ return self.get_csv(data, sep=sep)
elif self.export_type == "pdf":
- resp_data = self.get_pdf(data)
+ return self.get_pdf(data)
elif self.export_type == "html":
- resp_data = self.get_html_for_pdf(data)
- return resp_data
+ return self.get_html_for_pdf(data)
def get_filename(self):
"""
@@ -94,37 +150,46 @@ def get_filename(self):
now = datetime.now().strftime("%Y_%m_%d_%H_%M")
return f"export_{now}.{self.export_type}"
- def set_headers(self):
- """
- Set the headers for the response.
- """
- if self.export_type in ["pdf", "csv"]:
- self.request.response.setHeader(
- "Content-Disposition", f"attachment;filename={self.get_filename()}"
- )
- self.request.response.setHeader(
- "Content-Type", CONTENT_TYPES_MAPPING[self.export_type]
- )
-
- def get_csv(self, data):
+ def get_csv(self, data, encoding="utf-8-sig", sep=","):
"""
Generate CSV data from the provided data.
"""
+ # 1. Crea uno StringIO per il CSV
+ csv_buffer = StringIO()
+ # 2. Aggiungi l'header per il separatore (specifico per Excel)
+ # In Libreoffice viene aggiunta una riga, per ora evitiamo
+ # csv_buffer.write(f"sep={sep}\n")
+ # 3. Scrittura dei dati CSV
columns = self.get_columns(data)
-
- csv_data = StringIO()
- csv_writer = csv.writer(csv_data, quoting=csv.QUOTE_ALL)
+ csv_writer = csv.writer(csv_buffer, delimiter=sep, quoting=csv.QUOTE_ALL)
csv_writer.writerow([c["title"] for c in columns])
-
for item in data:
csv_writer.writerow(self.format_row(item))
- return csv_data.getvalue().encode("utf-8")
+ # 4. Prepara i bytes con BOM (UTF-8-sig)
+ csv_data = csv_buffer.getvalue()
+ if encoding == "utf-8-sig":
+ csv_bytes = b"\xef\xbb\xbf" + csv_data.encode("utf-8") # Aggiunge BOM
+ else:
+ csv_bytes = csv_data.encode(encoding)
+ # 5. Crea la risposta con gli header corretti
+ response = self.request.response
+ response.setHeader(
+ "Content-Disposition", f"attachment;filename={self.get_filename()}"
+ )
+ response.setHeader("Content-Type", f"text/csv; charset={encoding}")
+ return csv_bytes
def get_pdf(self, data):
html_str = self.get_html_for_pdf(data=data)
pdf_file = BytesIO()
HTML(string=html_str).write_pdf(pdf_file)
pdf_file.seek(0)
+ # 5. Crea la risposta con gli header corretti
+ response = self.request.response
+ response.setHeader(
+ "Content-Disposition", f"attachment;filename={self.get_filename()}"
+ )
+ response.setHeader("Content-Type", "application/pdf")
return pdf_file.read()
def get_data(self):
@@ -176,6 +241,7 @@ def get_html_for_pdf(self, data):
context=self,
request=self.request,
)
+
return view(rows=data, columns=columns)
def pdf_styles(self):
@@ -184,7 +250,11 @@ def pdf_styles(self):
)
def pdf_title(self):
- return None
+ context = self.context.context
+ site_title = api.portal.get_registry_record("plone.site_title")
+ if site_title:
+ return f"{site_title}: {context.Title()}"
+ return context.Title()
def pdf_description(self):
return None
@@ -203,3 +273,13 @@ def pdf_cell_format(self, column, value):
if re.match(r"^\d{4}-\d{2}-\d{2}T00:00:00$", value):
return {"type": "str", "value": value.split("T")[0]}
return {"type": "str", "value": str(value)}
+
+ def pdf_logo(self):
+ site_logo = api.portal.get_registry_record("plone.site_logo")
+ if site_logo:
+ return image_to_html(site_logo.decode())
+ return None
+
+ def pdf_datetime(self):
+ # TODO: valutare localizzazione della data
+ return datetime.now().strftime("%d/%m/%Y %H:%M")
diff --git a/src/iosanita/contenttypes/browser/searchblock.py b/src/iosanita/contenttypes/browser/searchblock.py
index 1ea3d79..006716b 100644
--- a/src/iosanita/contenttypes/browser/searchblock.py
+++ b/src/iosanita/contenttypes/browser/searchblock.py
@@ -4,10 +4,15 @@
from .export_view import IExportViewTraverser
from copy import deepcopy
from iosanita.contenttypes import _
+from plone.app.querystring.interfaces import IQuerystringRegistryReader
+from plone.intelligenttext.transforms import convertWebIntelligentPlainTextToHtml
+from plone.memoize import view
+from plone.registry.interfaces import IRegistry
from plone.restapi.interfaces import ISerializeToJson
from zExceptions import BadRequest
from zExceptions import NotFound
from zope.component import getMultiAdapter
+from zope.component import getUtility
from zope.interface import implementer
import logging
@@ -86,11 +91,16 @@ def _query_from_facets(self):
}
)
elif facet["type"] == "daterangeFacet":
+ daterange = self.request.form[facet["field"]["value"]].split(",")
+ if not daterange[0]:
+ daterange[0] = "1970-01-01"
+ if not daterange[1]:
+ daterange[1] = "2500-01-01"
query.append(
{
"i": facet["field"]["value"],
"o": "plone.app.querystring.operation.date.between",
- "v": self.request.form[facet["field"]["value"]].split(","),
+ "v": daterange,
}
)
elif facet["type"] == "selectFacet" and not facet["multiple"]:
@@ -110,7 +120,7 @@ def _query_from_facets(self):
}
)
else:
- logger.warning("DEBUG: filter %s not implemnted", facet)
+ logger.warning("DEBUG: filter %s not implemented", facet)
query.append(
{
"i": facet["field"]["value"],
@@ -128,7 +138,6 @@ def get_data(self):
# 4. fare la ricerca
# 5. fare export in csv/pdf a seconda del formato
"""
-
# 2. Get columns, base filters and sorting
columns = self.block_data.get("columns", [])
@@ -185,13 +194,14 @@ def get_data(self):
# XXX: consideriamo perĂ² che senza usare il serializzatore un utente potrebbe
# chiedere qualsiasi atttributo degli oggetti, senza un controllo fine
# sullo schema
- fullobjects = True
- self.request.form["b_size"] = 9999
- results = getMultiAdapter((results, self.request), ISerializeToJson)(
- fullobjects=fullobjects
- )
- for obj in results["items"]:
- yield [obj["title"]] + [obj.get(c["field"]) for c in columns]
+ if results:
+ fullobjects = True
+ self.request.form["b_size"] = 9999
+ results = getMultiAdapter((results, self.request), ISerializeToJson)(
+ fullobjects=fullobjects
+ )
+ for obj in results["items"]:
+ yield [obj["title"]] + [obj.get(c["field"]) for c in columns]
def get_columns(self, data):
# Il titolo va aggiunto di default come prima colonna ?
@@ -200,3 +210,52 @@ def get_columns(self, data):
return [{"key": "title", "title": _("Titolo")}] + [
{"key": c["field"], "title": c["title"]} for c in columns
]
+
+ @view.memoize
+ def _get_querystring(self):
+ # @querystring endpoint
+ context = self.context.context
+ registry = getUtility(IRegistry)
+ reader = getMultiAdapter((registry, self.request), IQuerystringRegistryReader)
+ reader.vocab_context = context
+ result = reader()
+ return result
+
+ # TODO: valutare eventuale titolo impostato sul blocco
+ # def pdf_title(self):
+
+ pdf_description_as_html = True
+
+ def pdf_description(self) -> str:
+ query = []
+ querystring_registry = self._get_querystring()
+ searchtext = self._query_from_searchtext()
+ if searchtext and searchtext[0].get("v"):
+ # TODO: translate
+ query.append(f"Ricerca per: {searchtext[0]['v']}")
+ for facet in self.block_data.get("facets") or []:
+ if "field" not in facet:
+ logger.warning("invalid facet %s", facet)
+ continue
+ if facet["field"]["value"] in self.request.form:
+ value = self.request.form[facet["field"]["value"]]
+ if value in ["null"]:
+ continue
+ # TODO: gestire campi particolari come: multipli, date, ...
+ index = querystring_registry["indexes"].get(facet["field"]["value"])
+ if index:
+ if "values" in index:
+ # TODO: per i valori multipli ?
+ # TODO: facciamo constraint o fallback come ora?
+ if value in index["values"] and index["values"][value].get(
+ "title"
+ ):
+ query.append(
+ f'{facet["field"]["label"]}: {index["values"][value]["title"]}'
+ )
+ continue
+ query.append(f'{facet["field"]["label"]}: {value}')
+ if query:
+ # TODO: translate
+ txt = "Filtri applicati:\n- " + ",\n- ".join(query)
+ return convertWebIntelligentPlainTextToHtml(txt)
diff --git a/src/iosanita/contenttypes/browser/static/export_pdf.css b/src/iosanita/contenttypes/browser/static/export_pdf.css
index ada17e8..0b7a248 100644
--- a/src/iosanita/contenttypes/browser/static/export_pdf.css
+++ b/src/iosanita/contenttypes/browser/static/export_pdf.css
@@ -1,29 +1,81 @@
@page {
+ /* size: A4 portrait; */
size: landscape;
+ margin-bottom: 3cm;
+ @bottom-right {
+ content: counter(page);
+ font-size: 75%;
+ width: 20%;
+ }
+ @bottom-left {
+ content: element(footer);
+ width: 80%;
+ }
+ .description.first-page {
+ display: none;
+ }
}
+@page:first {
+ margin-top: 2cm;
+ @top-left {
+ content: element(header);
+ margin-top: 0;
+ }
+ .description.first-page {
+ display: block;
+ }
+}
+
body {
font-family: "Titillium Web", Geneva, Tahoma, sans-serif;
- font-size: 16px;
- margin: 10px auto;
- padding: 0 10px;
+ font-size: 14px;
color: #1C2024;
}
+header {
+ position: running(header);
+ width: 100%;
+ display: flex;
+ align-items: center;
+}
+
+/* header .site_data {
+ display: flex;
+ width: 580px;
+ align-items: center;
+ justify-content: center;
+} */
+
+/* header hr {
+ display: flex;
+ width: 580px;
+ align-items: center;
+ justify-content: center;
+} */
+
+footer {
+ position: running(footer);
+ width: 100%;
+}
+
a {
color: #235295
}
h1 {
- font-weight: 700;
+ font-weight: 500;
+ font-size: 18px;
+}
+#logo {
+ padding-right: 30px;
}
-p.description {
+/* p.description {
font-size: 1.3333333333rem;
-}
+} */
table.export-table {
table-layout: fixed;
width: 100%;
- margin-bottom: 1rem;
box-sizing: border-box;
border-spacing: 2px;
}
diff --git a/src/iosanita/contenttypes/browser/templates/export_pdf.pt b/src/iosanita/contenttypes/browser/templates/export_pdf.pt
index 517c639..a67c349 100644
--- a/src/iosanita/contenttypes/browser/templates/export_pdf.pt
+++ b/src/iosanita/contenttypes/browser/templates/export_pdf.pt
@@ -2,21 +2,41 @@
+ "
+ i18n:domain="iosanita.contenttypes"
+>
+
${description}