Skip to content
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

Add custom action provider for direct PDF sharing via email #132

Merged
merged 25 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
2.4.0 (unreleased)
------------------

- #132 Add custom action provider for direct PDF sharing via email
- #131 Hookable action providers
- #130 Allow direct PDF download of generated report
- #129 Fix template error when the report contains invalidated samples
- #128 Fix AttributeError 'Verificators' on model.verifiers call
Expand Down
Empty file.
24 changes: 24 additions & 0 deletions src/senaite/impress/actions/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">

<!-- PDF Download Action Provider -->
<browser:page
name="impress_download_pdf"
for="*"
class=".providers.DownloadPDF"
permission="zope2.View"
layer="senaite.impress.interfaces.ILayer"
/>

<!-- Send report PDF Action Provider -->
<browser:page
name="impress_send_pdf"
for="*"
class=".providers.SendPDF"
permission="zope2.View"
layer="senaite.impress.interfaces.ILayer"
/>

</configure>
178 changes: 178 additions & 0 deletions src/senaite/impress/actions/providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-

import collections
from datetime import datetime

import six

from bika.lims import api
from bika.lims.api import mail as mailapi
from bika.lims.interfaces import IAnalysisRequest
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from senaite.impress import senaiteMessageFactory as _


class CustomAction(BrowserView):
"""Base class for custom actions
"""
def __init__(self, context, request):
self.context = context
self.request = request
self.uids = self.get_uids_from_request()

def get_uids_from_request(self):
"""Returns a list of uids from the request
"""
uids = self.request.get("uids", "")
if isinstance(uids, six.string_types):
uids = uids.split(",")
unique_uids = collections.OrderedDict().fromkeys(uids).keys()
return filter(api.is_uid, unique_uids)


class DownloadPDF(CustomAction):
"""Download Action
"""
def __init__(self, context, request):
super(DownloadPDF, self).__init__(context, request)

def __call__(self):
pdf = self.request.get("pdf")
data = "".join([x for x in pdf.xreadlines()])
return self.download(data)

def download(self, data, mime_type="application/pdf"):
# NOTE: The filename does not matter, because we are downloading
# the PDF in the Ajax handler with createObjectURL
self.request.response.setHeader(
"Content-Disposition", "attachment; filename=report.pdf")
self.request.response.setHeader("Content-Type", mime_type)
self.request.response.setHeader("Content-Length", len(data))
self.request.response.setHeader("Cache-Control", "no-store")
self.request.response.setHeader("Pragma", "no-cache")
self.request.response.write(data)


class SendPDF(CustomAction):
"""Email Action
"""
template = ViewPageTemplateFile("templates/send_pdf.pt")

def __init__(self, context, request):
super(SendPDF, self).__init__(context, request)
self.objects = map(api.get_object, self.uids)
self.action_url = "{}/{}".format(api.get_url(context), self.__name__)

def __call__(self):
if self.request.form.get("submitted", False):
self.send(REQUEST=self.request)
return self.template()

def add_status_message(self, message, level="info"):
"""Set a portal status message
"""
return self.context.plone_utils.addPortalMessage(message, level)

@property
def laboratory(self):
"""Laboratory object from the LIMS setup
"""
return api.get_setup().laboratory

def is_sample(self, obj):
"""Check if the given object is a sample
"""
return IAnalysisRequest.providedBy(obj)

def get_email_sender_address(self):
"""Sender email is either the lab email or portal email "from" address
"""
lab_email = self.laboratory.getEmailAddress()
portal_email = api.get_registry_record("plone.email_from_address")
return lab_email or portal_email or ""

def get_default_recipient_emails(self):
"""Return the default recipient emails
"""
emails = []
for obj in self.objects:
if not self.is_sample(obj):
continue
contact = obj.getContact()
if not contact:
continue
email = contact.getEmailAddress()
if email not in emails:
emails.append(email)
return ", ".join(filter(mailapi.is_valid_email_address, emails))

def get_default_cc_emails(self):
"""Return the default CC emails
"""
emails = []
for obj in self.objects:
if not self.is_sample(obj):
continue
for contact in obj.getCCContact():
if not contact:
continue
email = contact.getEmailAddress()
if email not in emails:
emails.append(email)
for email in obj.getCCEmails().split(","):
if email not in emails:
emails.append(email)
return ", ".join(filter(mailapi.is_valid_email_address, emails))

def get_default_subject(self):
"""Return the default subject
"""
return ", ".join(map(api.get_id, self.objects))

def get_default_body(self):
"""Return the default body text
"""
return ""

def get_default_pdf_filename(self):
"""Return the default filename of the attached PDF
"""
timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
return "Report-{}.pdf".format(timestamp)

def send(self, REQUEST=None):
email_to = self.request.get("email_to", "")
email_cc = self.request.get("email_cc", "")
email_subject = self.request.get("email_subject", "")
email_body = self.request.get("email_body", "")

pdf = self.request.get("pdf")
if not pdf:
message = _("PDF attachment is missing")
self.add_status_message(message, level="error")
return False

pdf_filename = self.request.get("pdf_filename")
# workaround for ZPublisher.HTTPRequest.FileUpload object
pdf_data = "".join(pdf.xreadlines())
pdf_attachment = mailapi.to_email_attachment(pdf_data, pdf_filename)

mime_msg = mailapi.compose_email(
self.get_email_sender_address(),
email_to,
email_subject,
email_body,
[pdf_attachment])
mime_msg["CC"] = mailapi.to_email_address(email_cc)

sent = mailapi.send_email(mime_msg)

if not sent:
message = _("Failed to send Email")
self.add_status_message(message, level="error")
return False

message = _("Email sent")
self.add_status_message(message, level="info")
return True
114 changes: 114 additions & 0 deletions src/senaite/impress/actions/templates/send_pdf.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<div class="send-pdf-modal modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" i18n:translate="">Send PDF by Email</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Show status messages inside the modal -->
<tal:message tal:content="structure provider:plone.globalstatusmessage"/>
<form name="send-pdf-form"
class="form"
method="POST"
enctype="multipart/form-data"
action="."
tal:attributes="action python:view.action_url">

<!-- TO -->
<div class="form-group row">
<label i18n:translate="label_email_to"
for="input-to"
class="col-sm-3 col-form-label">
To
</label>
<div class="col-sm-9">
<input type="email" multiple
required
tal:attributes="value python:request.get('email_to') or view.get_default_recipient_emails()"
name="email_to" autocomplete="off" class="form-control form-control-sm" id="input-to" />
</div>
</div>

<!-- CC -->
<div class="form-group row">
<label i18n:translate="label_email_cc"
for="input-cc"
class="col-sm-3 col-form-label">
CC
</label>
<div class="col-sm-9">
<input type="email" multiple
tal:attributes="value python:request.get('email_cc') or view.get_default_cc_emails()"
name="email_cc" autocomplete="off" class="form-control form-control-sm" id="input-email" />
</div>
</div>

<!-- SUBJECT -->
<div class="form-group form-group-sm row">
<label i18n:translate="label_email_subject"
for="input-subject"
class="col-sm-3 col-form-label">
Subject
</label>
<div class="col-sm-9">
<input type="text"
tal:attributes="value python:request.get('email_subject') or view.get_default_subject()"
name="email_subject" class="form-control form-control-sm" id="input-subject" />
</div>
</div>

<!-- BODY -->
<div class="form-group form-group-sm row">
<div class="col-sm-12">
<textarea name="email_body"
tal:content="python:request.get('email_body') or view.get_default_body()"
rows="5" class="form-control form-control-sm" id="input-body">
</textarea>
</div>
</div>

<!-- PDF Filename -->
<div class="form-group form-group-sm row">
<label i18n:translate="label_email_pdf_filename"
for="input-subject"
class="col-sm-3 col-form-label">
Filename
</label>
<div class="col-sm-9">
<input type="text"
value="Report.pdf"
tal:attributes="value python:request.get('pdf_filename') or view.get_default_pdf_filename()"
name="pdf_filename" class="form-control form-control-sm" id="input-filename" />
</div>
</div>

<div class="form-group mt-2">
<input class="btn btn-sm btn-primary"
type="submit"
name="send"
i18n:attributes="value"
value="Send Email" />
</div>

<!-- hidden fields -->
<input type="hidden" name="submitted" value="1" />
<input tal:replace="structure context/@@authenticator/authenticator"/>
<input type="hidden" name="uids" value="" tal:attributes="value request/uids|nothing" />

</form>
</div>
</div>

<!-- Modal helper JS -->
<script type="text/javascript">
console.info("*** SEND PDF Modal Loaded ***");
let form = document.querySelector("form[name='send-pdf-form']");
let button = form.querySelector("input[name='send']");
form.addEventListener("submit", (event) => {
// avoid double click
button.disabled = true;
});
</script>
</div>
Loading