-
Notifications
You must be signed in to change notification settings - Fork 0
export csv / pdf #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
export csv / pdf #22
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
04be0ed
wip
mamico 5c44c27
add generic export_view
cekk e38d0cb
blacked
cekk d012731
blacked
cekk 6560dd5
blacked
cekk d6a4e45
blacked
cekk 3414f6d
doc
mamico 3f36ce5
export_pdf
mamico 091c95d
css
mamico e05fead
bbb
73ae4f1
template
mamico 4194fc5
bbb
5060abe
bbb
6772e96
black
mamico deb1f68
ci
mamico 7ad404a
doc
mamico 602eecf
changelog
mamico 5aa3739
warning
f55a507
cleanup
mamico c95d84d
searchblock
2d4fd58
b_size
mamico fe0e169
cleanup
5e05579
default
mamico 407fc93
flake8
mamico 611f33c
cleanup
mamico File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| from datetime import datetime | ||
| from io import BytesIO | ||
| from io import StringIO | ||
| from iosanita.contenttypes import _ | ||
| from plone import api | ||
| from Products.Five.browser import BrowserView | ||
| from weasyprint import HTML | ||
| from zExceptions import BadRequest | ||
| from zope.interface import implementer | ||
| from zope.publisher.interfaces import IPublishTraverse | ||
|
|
||
| import csv | ||
| import importlib.resources | ||
| import logging | ||
| import re | ||
|
|
||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| fontools_logger = logging.getLogger("fontTools.subset") | ||
| fontools_logger.setLevel(logging.WARNING) | ||
|
|
||
|
|
||
| CONTENT_TYPES_MAPPING = { | ||
| "csv": "text/comma-separated-values", | ||
| "pdf": "application/pdf", | ||
| "html": "text/html", | ||
| } | ||
|
|
||
|
|
||
| class IExportViewTraverser(IPublishTraverse): | ||
| """ | ||
| Marker interface for Download views | ||
| """ | ||
|
|
||
|
|
||
| @implementer(IExportViewTraverser) | ||
| class ExportViewTraverser(BrowserView): | ||
| pass | ||
|
|
||
|
|
||
| class IExportViewDownload(IPublishTraverse): | ||
| pass | ||
|
|
||
|
|
||
| @implementer(IExportViewDownload) | ||
| class ExportViewDownload(BrowserView): | ||
| """ | ||
| @@download view that need to be called over a view that implements IExportViewTraverser | ||
|
|
||
| """ | ||
|
|
||
| def __init__(self, context, request): | ||
| super().__init__(context, request) | ||
| self.export_type = "csv" | ||
|
|
||
| def publishTraverse(self, request, name): | ||
| """ | ||
| e.g. | ||
| .../it/bandi/avvisi/view-name/@@download/csv?xxx=yyy | ||
| """ | ||
| self.export_type = name or "csv" | ||
| return self | ||
|
|
||
| def __call__(self): | ||
| """ """ | ||
| if self.export_type not in ["csv", "pdf", "html"]: | ||
| raise BadRequest( | ||
| api.portal.translate( | ||
| _( | ||
| "invalid_export_type", | ||
| default="Invalid export type: ${export_type}", | ||
| mapping={"export_type": self.export_type}, | ||
| ) | ||
| ) | ||
| ) | ||
| self.set_headers() | ||
| data = self.get_data() | ||
| if not data: | ||
| return "" | ||
| resp_data = "" | ||
| if self.export_type == "csv": | ||
| resp_data = self.get_csv(data) | ||
| elif self.export_type == "pdf": | ||
| resp_data = self.get_pdf(data) | ||
| elif self.export_type == "html": | ||
| resp_data = self.get_html_for_pdf(data) | ||
| return resp_data | ||
|
|
||
| def get_filename(self): | ||
| """ | ||
| Return the filename for the CSV export. | ||
| """ | ||
| 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): | ||
| """ | ||
| Generate CSV data from the provided data. | ||
| """ | ||
| columns = self.get_columns(data) | ||
|
|
||
| csv_data = StringIO() | ||
| csv_writer = csv.writer(csv_data, 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") | ||
|
|
||
| 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) | ||
| return pdf_file.read() | ||
|
|
||
| def get_data(self): | ||
mamico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Should be implemented in your view. | ||
|
|
||
| Returns: | ||
| list of objects: | ||
|
|
||
| Example: | ||
| [ | ||
| ["Mario", "22"], | ||
| ["Giovanna", "21"], | ||
| ] | ||
| """ | ||
| raise NotImplementedError() | ||
|
|
||
| def format_row(self, item): | ||
| """ """ | ||
| return item | ||
|
|
||
| def get_columns(self, data): | ||
| """ | ||
| Should be implemented in your view. | ||
|
|
||
| Args: | ||
| data: The input data used to determine headers | ||
| (type depends on implementation). | ||
|
|
||
| Returns: | ||
| list of dict: A list of header definitions, each represented as a dictionary with: | ||
| - "title" (str): The display name of the column. | ||
| - "key" (str): The corresponding field name in the data. | ||
| Example: | ||
| [ | ||
| {"title": "Name", "key": "name"}, | ||
| {"title": "Age", "key": "age"} | ||
| ] | ||
| """ | ||
| raise NotImplementedError() | ||
|
|
||
| def get_html_for_pdf(self, data): | ||
| """ | ||
| Generate HTML data from the provided data. | ||
| """ | ||
| columns = self.get_columns(data) | ||
| view = api.content.get_view( | ||
| name="export_pdf", | ||
| context=self, | ||
| request=self.request, | ||
| ) | ||
| return view(rows=data, columns=columns) | ||
|
|
||
| def pdf_styles(self): | ||
| return importlib.resources.read_text( | ||
| "iosanita.contenttypes.browser.static", "export_pdf.css" | ||
| ) | ||
|
|
||
| def pdf_title(self): | ||
| return None | ||
|
|
||
| def pdf_description(self): | ||
| return None | ||
|
|
||
| def pdf_cell_format(self, column, value): | ||
| if value is None: | ||
| return {"type": "str", "value": value} | ||
| if isinstance(value, dict): | ||
| # e.g. {'token': 'in_corso', 'title': 'In corso'} | ||
| return {"type": "str", "value": value.get("token")} | ||
| # XXX: this is a guess | ||
| if value.startswith("https://"): | ||
| return {"type": "url", "url": value, "value": column["title"]} | ||
| # XXX: this is a guess | ||
| # 2025-05-21T00:00:00 -> isoformat date YYYY-MM-DD | ||
| 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)} | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.