{"id":19175581,"url":"https://github.com/senaite/senaite.impress","last_synced_at":"2025-04-07T07:18:19.948Z","repository":{"id":31613575,"uuid":"123693023","full_name":"senaite/senaite.impress","owner":"senaite","description":"HTML to PDF Rendering Engine for SENAITE","archived":false,"fork":false,"pushed_at":"2024-10-21T08:12:29.000Z","size":14791,"stargazers_count":21,"open_issues_count":6,"forks_count":25,"subscribers_count":6,"default_branch":"2.x","last_synced_at":"2025-03-31T06:07:57.194Z","etag":null,"topics":["analysis-reports","coffeescript","lims","pdf","print","publication-engine","reactjs","senaite","weasyprint"],"latest_commit_sha":null,"homepage":"","language":"SCSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/senaite.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-03T13:11:22.000Z","updated_at":"2024-10-21T08:12:32.000Z","dependencies_parsed_at":"2022-07-27T15:19:07.416Z","dependency_job_id":"cca62878-3840-4e31-a52c-a2d6e202088c","html_url":"https://github.com/senaite/senaite.impress","commit_stats":{"total_commits":506,"total_committers":5,"mean_commits":101.2,"dds":0.03754940711462451,"last_synced_commit":"6100b7e23d6a58d286e40bd2f45dadd6e0b446ab"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senaite%2Fsenaite.impress","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senaite%2Fsenaite.impress/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senaite%2Fsenaite.impress/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/senaite%2Fsenaite.impress/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/senaite","download_url":"https://codeload.github.com/senaite/senaite.impress/tar.gz/refs/heads/2.x","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247608160,"owners_count":20965953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analysis-reports","coffeescript","lims","pdf","print","publication-engine","reactjs","senaite","weasyprint"],"created_at":"2024-11-09T10:24:03.162Z","updated_at":"2025-04-07T07:18:19.926Z","avatar_url":"https://github.com/senaite.png","language":"SCSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://github.com/senaite/senaite.impress\"\u003e\n    \u003cimg src=\"static/impress-logo.png\" alt=\"SENAITE IMPRESS\" height=\"64\" /\u003e\n  \u003c/a\u003e\n  \u003cp\u003ePublication of HTML/PDF Reports in SENAITE\u003c/p\u003e\n\n  \u003cdiv\u003e\n    \u003ca href=\"https://pypi.python.org/pypi/senaite.impress\"\u003e\n      \u003cimg src=\"https://img.shields.io/pypi/v/senaite.impress.svg?style=flat-square\" alt=\"pypi-version\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/senaite/senaite.impress/pulls\"\u003e\n      \u003cimg src=\"https://img.shields.io/github/issues-pr/senaite/senaite.impress.svg?style=flat-square\" alt=\"open PRs\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/senaite/senaite.impress/issues\"\u003e\n      \u003cimg src=\"https://img.shields.io/github/issues/senaite/senaite.impress.svg?style=flat-square\" alt=\"open Issues\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"#\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square\" alt=\"pr\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://www.senaite.com\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/Made%20for%20SENAITE-%E2%AC%A1-lightgrey.svg\" alt=\"Made for SENAITE\" /\u003e\n    \u003c/a\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\n## About\n\nSENAITE IMPRESS is basically a rendering engine for HTML documents to PDF. It\nsupports any kind of international paperformat with their corresponding paper\ndimensions, portrait and landscape orientation and merging of multiple PDFs to\none document.\n\n\n## Installation\n\nSENAITE IMPRESS is included in the SENAITE LIMS distribution, so no further\ninstallation steps are needed.\n\nTo learn more about how to install SENAITE, please follow the\n[SNEAITE Installation Manual](https://www.senaite.com/docs/installation)\n\n\n## Usage\n\nSENAITE IMPRESS opens automatically when a Sample is published, prepublished or\nrepupublished.\n\n\u003cimg src=\"static/verify_analyses.png\" alt=\"Verify Analyses\" /\u003e\n\nThe above screenshot shows 4 Analysis Requests in the workflow state \"Verified\".\nSelect them and click the \"Publish\" Button.\n\n\u003cimg src=\"static/publish_view.png\" alt=\"Publish View\" /\u003e\n\nThe SENAITE IMPRESS preview allows you to change the report template,\npaperformat and the orientation.\n\nThe \"merge\" checkbox creates a single PDF, even when no multi-template was selected.\n\nThe button \"Save\" will do the following actions:\n\n- Generate the PDF and store it below the corresponding Analysis Request\n- Redirect to the \"Analysis Reports\" view of the customer\n- Keep the current Workflow state (Publication is only done by sending the Email)\n\nThe button \"Email\" will do the same actions as \"Save\", but redirects directly to the Email view.\n\n\n### Analysis Reports Listing View\n\nA new tab *Analysis Reports* will be available for Clients, which lists all\ngenerated PDF reports for all Analysis Requests of this client. Reports can be\nselected in this listing for email delivery. This also allows to send multiple\nPDF Analysis Reports in a single Email.\n\n\u003cimg src=\"static/analysis_reports.png\" alt=\"Analysis Reports\" /\u003e\n\nSelecting one or more Reports allows to send the generated PDFs to the selected\ncontacts of the Analysis Request.\n\n\u003cimg src=\"static/email_view.png\" alt=\"Email View\" /\u003e\n\nIf the Email was successfully sent, the corresponding Analysis Requests change\nthe state to \"Published\".\n\n\u003cimg src=\"static/published.png\" alt=\"Published\" /\u003e\n\nAfter you clicked the \"Send\" button, the Email with the attched reports are\ndelivered to the selected recipients.\n\nThe workflow states of the Analysis Requests have changed their state to \"Published\".\n\n\n**Note**\n\nIn the case that multiple Analysis Requests are rendered in a single Report, the\ncontained Analysis Requests are also published when this Report was send.\n\n\n## Custom Reports\n\nMost of the labs require custom reports and SENAITE IMPRESS allows you to do\nthat with relative ease.\n\nThe following sections will guide you through the process of creating a custom report.\n\n\n### Hello World\n\nThe easiest way to get started with `senaite.impress` is to copy one of the\nexisting templates in the `templates/reports` folder within this package.\n\n\nThe smallest report example looks like this:\n\n```html\n\u003ch1\u003eHello World!\u003c/h1\u003e\n```\n\nIt renders a heading saying “Hello, world!” on the report.\n\n\u003cimg src=\"static/1_hello_world.png\" alt=\"Hello World\" /\u003e\n\nThe next few sections will gradually introduce you to using `senaite.impress`.\nWe will examine single- and multi reports, Zope page templates and the super model.\nOnce you master them, you can create complex reports for SENAITE.\n\n\n### Single/Multi Reports\n\nThe difference between single- and multi reports is that a single reports\nreceive a single report object, while multi reports receive a collection of\nreport objects.\n\n`senaite.impress` uses the report name to distinguish between a single- and\nmulti report. A report starting or ending with the workd `Multi`, e.g.\n`MultiReport.pt` or `PublicationReportMulti.pt` will be considered as a multi\nreport and it will receive all selected objects in a `collection`.\n\nAll other reports, e.g. `Default.pt`, `HelloWorld.pt`, `SingleReport.pt` will be\nconsidered as single reports and it will receive the single report as its `model`.\n\nThe most basic single report looks like this:\n\n```html\n\u003ctal:report define=\"model python:view.model;\"\u003e\n  \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n\u003c/tal:report\u003e\n```\n\nIt renders the ID of the model (in this case the Analysis Request `H2O-0001-R01`) on the report.\n\n\u003cimg src=\"static/2_single_report.png\" alt=\"Single Report\" /\u003e\n\nTo render a multi report, we need to copy the previous template to `MultiHelloWorld.pt`.\n\nThe most basic multi report looks like this:\n\n```html\n\u003ctal:report define=\"collection python:view.collection;\"\u003e\n  \u003ctal:model tal:repeat=\"model collection\"\u003e\n    \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n  \u003c/tal:model\u003e\n\u003c/tal:report\u003e\n```\n\nIt renders the IDs of the model (in this case the Analysis Requests\n`H2O-0001-R01` and `H2O-0002-R01`) on the same report.\n\n\u003cimg src=\"static/3_multi_report.png\" alt=\"Multi Report\" /\u003e\n\nChange between the templates `HelloWorld.pt` and `MultiHelloWorld.pt` to see how\nthe two selected Analysis Requests render either on two pages or on one page.\n\n\n### Zope Page Templates\n\n[Zope Page Templates](http://zope.readthedocs.io/en/latest/zope2book/ZPT.html)\nis the main web page generation tool in SENAITE.\n\nPage Templates are recommended way to generate reports in `senaite.impress`.\nWe have already seen a small example how to use the Template Attribute Language\n(TAL). TAL consists of special tag attributes. For example, we used a dynamic\npage headline in the previous reports:\n\n```html\n\u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n```\n\n### Super Model\n\nThe [Super Model](https://github.com/senaite/senaite.app.supermodel#readme) is\na special wrapper object for database objects in SENAITE.\n\nThe advantage of Super Models is that they provide transparent access to all\ncontent schema fields in a preformance optimized way.\n\nAlso see: https://github.com/senaite/senaite.app.supermodel#readme\n\nFor example the content type\n[Analysis Request](https://github.com/senaite/senaite.core/blob/master/bika/lims/content/analysisrequest.py)\nin SENAITE defines a computed field `SampleTypeTitle`.\n\nTo access this field in a report, you simply traverse it by name:\n\n```html\n\u003ctal:report define=\"model python:view.model;\"\u003e\n  \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n  \u003ch2\u003e\n    Sample Type:\n    \u003cspan tal:content=\"model/SampleTypeTitle\"\u003e\n      This will be replaced with the Sample Type Title\n    \u003c/span\u003e\n  \u003c/h2\u003e\n\u003c/tal:report\u003e\n```\n\nNow it should render the title of the sample type below the ID of the Analysis Request:\n\n\u003cimg src=\"static/4_report_model.png\" alt=\"Super Model\" /\u003e\n\n\n### Bootstrap\n\n`senaite.impress` uses [Bootstrap 4](https://getbootstrap.com) as the main front-end component library.\nEach report will therefore follow these style guidelines and can be easily extended.\n\nPlease note, that you should start with\n[Rows](https://getbootstrap.com/docs/4.0/layout/grid/#how-it-works) as the top\nlevel HTML element inside a report to maintain the borders of the selected paper\nformat.\n\n```html\n\u003ctal:report define=\"model python:view.model;\"\u003e\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"col-sm-12\"\u003e\n      \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n      \u003ch2\u003e\n        Sample Type:\n        \u003cspan class=\"text-secondary\"\n              tal:content=\"model/SampleTypeTitle\"\u003e\n          This will be replaced with the Sample Type Title\n        \u003c/span\u003e\n      \u003c/h2\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/tal:report\u003e\n```\n\n\u003cimg src=\"static/5_bootstrap.png\" alt=\"Bootstrap\" /\u003e\n\n\n### Customizing the report design\n\nTo customize the style of your report, it is recommended to add the CSS style inline.\n\n```html\n\u003ctal:report define=\"model python:view.model;\"\u003e\n\n  \u003ctal:css define=\"laboratory view/laboratory;\"\u003e\n    \u003cstyle type=\"text/css\"\u003e\n     html, body { font-size: 1em; }\n     h1 { font-size: 160%; }\n     h2 { font-size: 120%; }\n     @page {\n       font-size: 9pt;\n       @top-left {\n         content: '\u003cspan tal:omit-tag=\"\" tal:content=\"laboratory/Name\"/\u003e';\n       }\n       @top-right {\n         content: \"\u003ctal:t i18n:translate=''\u003ePage\u003c/tal:t\u003e \" counter(page) \" \u003ctal:t i18n:translate=''\u003eof\u003c/tal:t\u003e \" counter(pages);\n       }\n     }\n    \u003c/style\u003e\n  \u003c/tal:css\u003e\n\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"col-sm-12\"\u003e\n      \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n      \u003ch2\u003e\n        Sample Type:\n        \u003cspan class=\"text-secondary\"\n              tal:content=\"model/SampleTypeTitle\"\u003e\n          This will be replaced with the Sample Type Title\n        \u003c/span\u003e\n      \u003c/h2\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/tal:report\u003e\n```\n\nPlease also see the [Paged media](https://developer.mozilla.org/en-US/docs/Web/CSS/Paged_Media)\nCSS properties to learn how to control the presentation of content for print or\nany other media that splits content into discrete pages.\n\n\u003cimg src=\"static/6_custom_css.png\" alt=\"Custom CSS\" /\u003e\n\n\n### Report View\n\nThe Report View acts as a controller for the multi- and single reports. It\nprovides code logic to group, sort and extract the data of the report model/collection.\n\nMethods (functions) of the Report view are referenced by the keyword `view` in the template\nand provide the business controller logic between the plain data object and SENAITE LIMS/HEALTH.\n\nReport views can be customized per report for any specific report behavior and model.\n\nThe standard report view for models of the type Analysis Request is located here:\nhttps://github.com/senaite/senaite.impress/blob/master/src/senaite/impress/analysisrequest/reportview.py\n\n```html\n\u003ctal:report define=\"model python:view.model;\"\u003e\n\n  \u003ctal:css define=\"laboratory view/laboratory;\"\u003e\n    \u003cstyle type=\"text/css\"\u003e\n     html, body { font-size: 1em; }\n     h1 { font-size: 160%; }\n     h2 { font-size: 120%; }\n     @page {\n       font-size: 9pt;\n       @top-left {\n         content: '\u003cspan tal:omit-tag=\"\" tal:content=\"laboratory/Name\"/\u003e';\n       }\n       @top-right {\n         content: \"\u003ctal:t i18n:translate=''\u003ePage\u003c/tal:t\u003e \" counter(page) \" \u003ctal:t i18n:translate=''\u003eof\u003c/tal:t\u003e \" counter(pages);\n       }\n     }\n    \u003c/style\u003e\n  \u003c/tal:css\u003e\n\n  \u003cdiv class=\"row\"\u003e\n    \u003cdiv class=\"col-sm-12\"\u003e\n      \u003ch1 tal:content=\"model/id\"\u003eThis will be replaced with the ID of the model\u003c/h1\u003e\n      \u003ch2\u003e\n        Sample Type:\n        \u003cspan class=\"text-secondary\"\n              tal:content=\"model/SampleTypeTitle\"\u003e\n          This will be replaced with the Sample Type Title\n        \u003c/span\u003e\n      \u003c/h2\u003e\n      \u003chr class=\"py-1\"/\u003e\n      \u003cdiv class=\"text-muted font-italic\"\u003e\n        Published \u003cspan tal:content=\"python:view.to_localized_time(model.DatePublished)\"/\u003e\n        by \u003cspan tal:content=\"python:view.current_user.get('fullname')\"/\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/tal:report\u003e\n```\n\n\u003cimg src=\"static/7_report_view.png\" alt=\"Report View\" /\u003e\n\n\n### Reports in external packages\n\nUntil now we created all reports on the file system within this package, which\nis **not** the recommended way, because with future updates of\n`senaite.impress`, these changes will be lost.\n\nTherefore it is recommended to create a new\n[SENAITE Add-On Package](https://docs.plone.org/4/en/develop/addons/schema-driven-forms/creating-a-simple-form/creating-a-package.html)\nand put the custom reports in there. Note that the naming of the report template\nis important if you are customising a multi-sample report - you MUST start or\nend the template name with the word 'multi'.\n\nIn your new package `configure.zcml` you have to specify the folder where your reports live:\n\n```xml\n\u003cconfigure\n    xmlns=\"http://namespaces.zope.org/zope\"\n    xmlns:plone=\"http://namespaces.plone.org/plone\"\u003e\n\n  \u003c!-- Report resource directory --\u003e\n  \u003cplone:static\n      directory=\"reports\"\n      type=\"senaite.impress.reports\"/\u003e\n\n\u003c/configure\u003e\n```\n\nThis will integrate the `reports` directory within your package into the search\npath of `senaite.impress`.\n\nIt is recommended to have a report controller view in place to avoid heavy\nPython logic in the domain of the page template.\n\nTo create a custom report controller view, you need to have first a custom\nbrowser layer defined in your `interfaces.py`:\n\n```python\nfrom bika.lims.interfaces import IBikaLIMS\nfrom senaite.impress.interfaces import ILayer as ISenaiteIMPRESS\nfrom senaite.lims.interfaces import ISenaiteLIMS\n\n\nclass IMyLIMSLayer(IBikaLIMS, ISenaiteLIMS, ISenaiteIMPRESS):\n    \"\"\"Marker interface that defines a Zope 3 browser layer.\n    \"\"\"\n```\n\nand register the layer in `profiles/default/browserlayer.xml`:\n```xml\n\u003clayers\u003e\n  \u003clayer name=\"my.lims\"\n         interface=\"my.lims.interfaces.IMyLIMSLayer\" /\u003e\n\u003c/layers\u003e\n``**\n\n\nThen you can register the controller view in `configure.zcml`:\n\n```xml\n  \u003c!-- View for Single Reports --\u003e\n  \u003cadapter\n      for=\"zope.interface.Interface\n           my.lims.interfaces.IMyLIMS\"\n      name=\"AnalysisRequest\"\n      factory=\".reportview.MySingleReportView\"\n      provides=\"senaite.impress.interfaces.IReportView\"\n      permission=\"zope2.View\"/\u003e\n\n  \u003c!-- View for Multi Reports --\u003e\n  \u003cadapter\n      for=\"zope.interface.Interface\n           my.lims.interfaces.IMyLIMSLayer\"\n      name=\"AnalysisRequest\"\n      factory=\".reportview.MyMultiReportView\"\n      provides=\"senaite.impress.interfaces.IMultiReportView\"\n      permission=\"zope2.View\"/\u003e\n```\n\n\nAnd create your own reportview.py module:\n\n```python\nfrom senaite.impress.analysisrequest.reportview import MultiReportView\n\nclass MyMultiReportView(MultiReportView):\n    \"\"\"My specific controller view for multi-reports\n    \"\"\"\n\n    def __init__(self, collection, request):\n        logger.info(\"MyMultiReportView::__init__:collection={}\"\n                    .format(collection))\n        super(MultiReportView, self).__init__(collection, request)\n        self.collection = collection\n        self.request = request\n```\n\n**Note**:\nSince version 1.2.4 it is also possible to register a multiadapter that adapts\nthe current rendering context, the report model/collection and the request:\n\n```xml\n  \u003c!-- View for Single Reports --\u003e\n  \u003cadapter\n      for=\"*\n           *\n           my.lims.interfaces.IMyLIMS\"\n      name=\"AnalysisRequest\"\n      factory=\".reportview.MySingleReportView\"\n      provides=\"senaite.impress.interfaces.IReportView\"\n      permission=\"zope2.View\"/\u003e\n\n  \u003c!-- View for Multi Reports --\u003e\n  \u003cadapter\n      for=\"*\n           *\n           my.lims.interfaces.IMyLIMSLayer\"\n      name=\"AnalysisRequest\"\n      factory=\".reportview.MyMultiReportView\"\n      provides=\"senaite.impress.interfaces.IMultiReportView\"\n      permission=\"zope2.View\"/\u003e\n```\n\nThis would allow to use the current context where `publish` view was called\ncan be used as well:\n\n```python\nfrom senaite.impress.analysisrequest.reportview import MultiReportView\n\nclass MyMultiReportView(MultiReportView):\n    \"\"\"My specific controller view for multi-reports\n    \"\"\"\n\n    def __init__(self, context, collection, request):\n        logger.info(\"MyMultiReportView::__init__:collection={}\"\n                    .format(collection))\n        super(MultiReportView, self).__init__(collection, request)\n        self.collection = collection\n        self.context = context\n        self.request = request\n```\n\n\n### Further Reading\n\n`senaite.impress` comes with some default templates included. It is recommended\nto read the code of these templates or use them as the base for new reports.\n\n\n### senaite.impress:Default.pt\n\nThis page template is renders single reports (one AR per report).\n\n\u003cimg src=\"static/8_default_template.png\" alt=\"senaite.impress:Default.pt\" /\u003e\n\n\n### senaite.impress:MultiDefault.pt\n\nThis page template is renders multiple reports on one report. The header and\nfooter will be rendered only once. The metadata of the first model (here the\nAnalysis Request `H20-0001-R01`) will be used for these sections and the\nresults/remarks/attachments sections will be repeated for all models in the\ncollection (`H20-0001-R01` and `H20-0002-R01`).\n\n\u003cimg src=\"static/9_multi_default_template.png\" alt=\"senaite.impress:MultiDefault.pt\" /\u003e\n\n\n### senaite.impress:MultiDefaultByColumn.pt\n\nThis page template behaves like the `senaite.impress:MultiDefault.pt`, except\nthat the results of all models (Analysis Requests) will be rendered in columns\nside by side.\n\n\u003cimg src=\"static/10_multi_default_by_column_template.png\" alt=\"senaite.impress:MultiDefaultByColumn.pt\" /\u003e\n\n\n## License\n\n**SENAITE.IMPRESS** Copyright (C) RIDING BYTES \u0026 NARALABS\n\nThis program is free software; you can redistribute it and/or modify it under\nthe terms of the [GNU General Public License version\n2](https://github.com/senaite/senaite.impress/blob/master/LICENSE)\nas published by the Free Software Foundation.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsenaite%2Fsenaite.impress","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsenaite%2Fsenaite.impress","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsenaite%2Fsenaite.impress/lists"}