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

Improved Email Publication #1413

Merged
merged 36 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6f8fc56
Better translation string
ramonski Jul 23, 2019
d4dab25
Publish samples first
ramonski Jul 23, 2019
1ea4a8d
Refactored to properties
ramonski Jul 23, 2019
c97aca9
Use email API to send email
ramonski Jul 23, 2019
cea5233
Refactored to properties
ramonski Jul 23, 2019
89b0600
Make send toggle editable
ramonski Jul 23, 2019
39ccd66
Moved property up
ramonski Jul 23, 2019
41aac28
Added plone.protect
ramonski Jul 23, 2019
eee4741
Refcatored emailview to use email API and improved logic
ramonski Jul 25, 2019
7f8818b
Fixed doctests
ramonski Jul 25, 2019
25ce7bd
Make analyses attachments addable to email
ramonski Jul 25, 2019
4376d80
Lookup max email size from registry
ramonski Jul 25, 2019
78d6098
Better icons for metadata + sendlog
ramonski Jul 25, 2019
9fca018
Formatting and comments
ramonski Jul 25, 2019
08974c4
Changelog updated
ramonski Jul 25, 2019
8cf4f31
Disable xvflb start to fix travis CI
ramonski Jul 25, 2019
46fb4d2
Handle unicode in email addresses
ramonski Jul 25, 2019
57cb2a6
Added icons for ARReport
ramonski Jul 26, 2019
b5b4944
Manually take a snapshot instead of calling processForm
ramonski Jul 26, 2019
fcc20fa
Publish the primary sample first in case the contained samples was no…
ramonski Jul 26, 2019
7c1b69c
Comments only
ramonski Jul 26, 2019
52035bb
Show the logline independently from other metadata
ramonski Jul 26, 2019
f52943d
Keep track of the Email text on submission
ramonski Jul 26, 2019
2b9a4c3
Merge branch 'master' into emailview2
ramonski Jul 27, 2019
f6f507b
Added info popup for reports
ramonski Jul 27, 2019
dbedebb
Make sendlog a records field
ramonski Jul 28, 2019
5f84825
Remove inline auditlog
ramonski Jul 28, 2019
02c46a7
Handle sendlog records in info popup
ramonski Jul 28, 2019
48b2945
Show send history in info popup
ramonski Jul 28, 2019
b9fc473
minor changes
ramonski Jul 28, 2019
515e706
Changed Actor to "Sender"
ramonski Jul 28, 2019
20fc972
Formatting only
ramonski Jul 28, 2019
eb78771
Removed form authenticator
ramonski Jul 29, 2019
ed10e4d
Use security API to get the current user and userid
ramonski Jul 29, 2019
7c2df67
Handle deleted Attachments
ramonski Jul 30, 2019
93c301c
Added UID as image title
ramonski Jul 30, 2019
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 CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Changelog

**Changed**

- #1413 Improved Email Publication


**Removed**

Expand Down
76 changes: 37 additions & 39 deletions bika/lims/api/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.Utils import formataddr
from email.Utils import parseaddr
from smtplib import SMTPException
from string import Template
from StringIO import StringIO
Expand Down Expand Up @@ -47,6 +48,18 @@ def to_email_address(address, name=""):
return formataddr(pair)


def parse_email_address(address):
"""Parse a given name/email pair

:param address: The name/email string to parse
:type address: basestring
:returns: Tuple of (name, email)
"""
if not isinstance(address, basestring):
raise ValueError("Expected a string, got {}".format(type(address)))
return parseaddr(address)


def to_email_subject(subject):
"""Convert the given subject to an email subject

Expand All @@ -70,44 +83,53 @@ def to_email_body_text(body, **kw):
return MIMEText(body_template, _subtype="plain", _charset="utf8")


def to_email_attachment(file_or_path, filename="", **kw):
def to_email_attachment(filedata, filename="", **kw):
"""Create a new MIME Attachment

The Content-Type: header is build from the maintype and subtype of the
guessed filename mimetype. Additional parameters for this header are
taken from the keyword arguments.

:param file_or_path: OS-level file or absolute path
:type file_or_path: str, FileIO, MIMEBase
:param filedata: File, file path, filedata
:type filedata: FileIO, MIMEBase, basestring
:param filename: Filename to use
:type filedata: str
:type filename: str
:returns: MIME Attachment
"""
filedata = ""
data = ""
maintype = "application"
subtype = "octet-stream"

def is_file(s):
try:
return os.path.exists(s)
except TypeError:
return False

# Handle attachment
if isinstance(file_or_path, MIMEBase):
if isinstance(filedata, MIMEBase):
# return immediately
return file_or_path
return filedata
# Handle file/StringIO
elif isinstance(file_or_path, (file, StringIO)):
filedata = file_or_path.read()
# Handle file path
elif os.path.isfile(file_or_path):
filename = filename or os.path.basename(file_or_path)
with open(file_or_path, "r") as f:
elif isinstance(filedata, (file, StringIO)):
data = filedata.read()
# Handle file paths
if is_file(filedata):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do elif here

filename = filename or os.path.basename(filedata)
with open(filedata, "r") as f:
# read the filedata from the filepath
filedata = f.read()
data = f.read()
# Handle raw filedata
elif isinstance(filedata, basestring):
data = filedata

# Set MIME type from keyword arguments or guess it from the filename
mime_type = kw.pop("mime_type", None) or mimetypes.guess_type(filename)[0]
if mime_type is not None:
maintype, subtype = mime_type.split("/")

attachment = MIMEBase(maintype, subtype, **kw)
attachment.set_payload(filedata)
attachment.set_payload(data)
encoders.encode_base64(attachment)
attachment.add_header("Content-Disposition",
"attachment; filename=%s" % filename)
Expand All @@ -132,30 +154,6 @@ def is_valid_email_address(address):
return True


def parse_email_address(address):
"""Parse a given name/email pair

:param address: The name/email string to parse
:type address: basestring
:returns: RFC 2822 email address
"""
if not isinstance(address, basestring):
raise ValueError("Expected a string, got {}".format(type(address)))

# parse <name>, <email> recipient
splitted = map(lambda s: s.strip(),
safe_unicode(address).rsplit(",", 1))

pair = []
for s in splitted:
if is_valid_email_address(s):
pair.insert(0, s)
else:
pair.append(s)

return to_email_address(*pair)


def compose_email(from_addr, to_addr, subj, body, attachments=[], **kw):
"""Compose a RFC 2822 MIME message

Expand Down
112 changes: 112 additions & 0 deletions bika/lims/browser/analysisreport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.CORE.
#
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2018-2019 by it's authors.
# Some rights reserved, see README and LICENSE.

from bika.lims import api
from bika.lims.browser import BrowserView
from bika.lims.utils import get_image
from plone.memoize import view
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from ZODB.POSException import POSKeyError


class AnalysisReportInfoView(BrowserView):
"""Show details of the Analysis Report
"""
template = ViewPageTemplateFile("templates/analysisreport_info.pt")

def __init__(self, context, request):
super(AnalysisReportInfoView, self).__init__(context, request)

def __call__(self):
# disable the editable border
self.request.set("disable_border", 1)
return self.template()

@view.memoize
def get_report(self):
report_uid = self.request.form.get("report_uid")
return api.get_object_by_uid(report_uid, None)

@view.memoize
def get_primary_sample(self):
report = self.get_report()
return report.getAnalysisRequest()

@view.memoize
def get_metadata(self):
report = self.get_report()
return report.getMetadata()

@view.memoize
def get_sendlog(self):
report = self.get_report()
records = report.getSendLog()
return list(reversed(records))

@view.memoize
def get_contained_samples(self):
metadata = self.get_metadata()
samples = metadata.get("contained_requests", [])
sample_uids = filter(api.is_uid, samples)
return map(api.get_object_by_uid, sample_uids)

def get_icon_for(self, typename):
"""Returns the big image icon by its (type-)name
"""
image = "{}_big.png".format(typename)
return get_image(
image, width="20px", style="vertical-align: baseline;")

def get_filesize(self, f):
"""Return the filesize of the PDF as a float
"""
try:
filesize = float(f.get_size())
return float("%.2f" % (filesize / 1024))
except (POSKeyError, TypeError, AttributeError):
return 0.0

@view.memoize
def get_attachment_data_by_uid(self, uid):
"""Retrieve attachment data by UID
"""
attachment = api.get_object_by_uid(uid, default=None)
# Attachment file not found/deleted
if attachment is None:
return {}
f = attachment.getAttachmentFile()
attachment_type = attachment.getAttachmentType()
attachment_keys = attachment.getAttachmentKeys()
filename = f.filename
filesize = self.get_filesize(f)
mimetype = f.getContentType()
report_option = attachment.getReportOption()

return {
"obj": attachment,
"attachment_type": attachment_type,
"attachment_keys": attachment_keys,
"file": f,
"uid": uid,
"filesize": filesize,
"filename": filename,
"mimetype": mimetype,
"report_option": report_option,
}
17 changes: 17 additions & 0 deletions bika/lims/browser/analysisreport.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="senaite.core">

<!-- Analysis Report Info View
(used in report listings for Popup Overlays) -->
<browser:page
for="*"
name="analysisreport_info"
class="bika.lims.browser.analysisreport.AnalysisReportInfoView"
permission="zope.Public"
layer="bika.lims.interfaces.IBikaLIMS"
/>

</configure>
1 change: 1 addition & 0 deletions bika/lims/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<include file="analysis.zcml"/>
<include file="analysisprofile.zcml"/>
<include file="analysisreport.zcml"/>
<include file="analysisservice.zcml"/>
<include file="analysisspec.zcml"/>
<include file="arimports.zcml"/>
Expand Down
Binary file added bika/lims/browser/images/arreport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bika/lims/browser/images/arreport_big.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bika/lims/browser/images/email.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bika/lims/browser/images/email_big.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading