Skip to content

Commit

Permalink
Add custom action provider for direct PDF sharing via email (#132)
Browse files Browse the repository at this point in the history
* Allow to hook custom actions

* Allow Modals

* Allow direct submit

* Code naming refactoring

* Production JS

* Changelog updated

* Removed modal

* Typo

* Allow HTML button text

* Implemented first draft of the send email modal

* Use current target for event

* Refactored action lookup

* config for close after submit

* debugging

* Support status messages

* Render custom actions first

* Better default values

* Disable send button once clicked

* Added configuration option to enable direct pdf sharing

* Generated production JS

* Changelog updated

* Fix missing statusmessage in FF

* Fixed missing fat arrow in coffesscript

* Refactor available attribute to be a method

* Removed attribute
  • Loading branch information
ramonski authored Feb 7, 2023
1 parent dff4d17 commit e246126
Show file tree
Hide file tree
Showing 19 changed files with 715 additions and 67 deletions.
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

0 comments on commit e246126

Please sign in to comment.