| Total Complexity | 91 |
| Total Lines | 659 |
| Duplicated Lines | 6.22 % |
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like bika.lims.browser.publish.emailview often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # -*- coding: utf-8 -*- |
||
| 2 | # |
||
| 3 | # This file is part of SENAITE.CORE. |
||
| 4 | # |
||
| 5 | # SENAITE.CORE is free software: you can redistribute it and/or modify it under |
||
| 6 | # the terms of the GNU General Public License as published by the Free Software |
||
| 7 | # Foundation, version 2. |
||
| 8 | # |
||
| 9 | # This program is distributed in the hope that it will be useful, but WITHOUT |
||
| 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
||
| 11 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
||
| 12 | # details. |
||
| 13 | # |
||
| 14 | # You should have received a copy of the GNU General Public License along with |
||
| 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 |
||
| 16 | # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||
| 17 | # |
||
| 18 | # Copyright 2018-2019 by it's authors. |
||
| 19 | # Some rights reserved, see README and LICENSE. |
||
| 20 | |||
| 21 | import inspect |
||
| 22 | import mimetypes |
||
| 23 | import socket |
||
| 24 | from collections import OrderedDict |
||
| 25 | from email import encoders |
||
| 26 | from email.header import Header |
||
| 27 | from email.mime.base import MIMEBase |
||
| 28 | from email.mime.multipart import MIMEMultipart |
||
| 29 | from email.mime.text import MIMEText |
||
| 30 | from email.Utils import formataddr |
||
| 31 | from smtplib import SMTPException |
||
| 32 | from string import Template |
||
| 33 | |||
| 34 | from bika.lims import logger |
||
| 35 | from bika.lims.utils import to_utf8 |
||
| 36 | from Products.CMFCore.WorkflowCore import WorkflowException |
||
| 37 | from Products.CMFPlone.utils import safe_unicode |
||
| 38 | from Products.Five.browser import BrowserView |
||
| 39 | from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
||
| 40 | from bika.lims import api |
||
| 41 | from bika.lims import _ |
||
| 42 | from bika.lims.decorators import returns_json |
||
| 43 | from ZODB.POSException import POSKeyError |
||
| 44 | from zope.interface import implements |
||
| 45 | from zope.publisher.interfaces import IPublishTraverse |
||
| 46 | |||
| 47 | EMAIL_MAX_SIZE = 15 |
||
| 48 | |||
| 49 | |||
| 50 | class EmailView(BrowserView): |
||
| 51 | """Email Attachments View |
||
| 52 | """ |
||
| 53 | implements(IPublishTraverse) |
||
| 54 | |||
| 55 | template = ViewPageTemplateFile("templates/email.pt") |
||
| 56 | email_template = ViewPageTemplateFile("templates/email_template.pt") |
||
| 57 | |||
| 58 | def __init__(self, context, request): |
||
| 59 | super(EmailView, self).__init__(context, request) |
||
| 60 | # disable Plone's editable border |
||
| 61 | request.set("disable_border", True) |
||
| 62 | # remember context/request |
||
| 63 | self.context = context |
||
| 64 | self.request = request |
||
| 65 | self.url = self.context.absolute_url() |
||
| 66 | # the URL to redirect on cancel or after send |
||
| 67 | self.exit_url = "{}/{}".format(self.url, "reports_listing") |
||
| 68 | # we need to transform the title to unicode, so that we can use it for |
||
| 69 | self.client_name = safe_unicode(self.context.Title()) |
||
| 70 | self.email_body = self.context.translate(_(self.email_template(self))) |
||
| 71 | # string interpolation later |
||
| 72 | # N.B. We need to translate the raw string before interpolation |
||
| 73 | subject = self.context.translate(_("Analysis Results for {}")) |
||
| 74 | self.email_subject = subject.format(self.client_name) |
||
| 75 | self.allow_send = True |
||
| 76 | self.traverse_subpath = [] |
||
| 77 | |||
| 78 | def __call__(self): |
||
| 79 | # handle subpath request |
||
| 80 | if len(self.traverse_subpath) > 0: |
||
| 81 | return self.handle_ajax_request() |
||
| 82 | # handle standard request |
||
| 83 | return self.handle_http_request() |
||
| 84 | |||
| 85 | def publishTraverse(self, request, name): |
||
| 86 | """Called before __call__ for each path name |
||
| 87 | """ |
||
| 88 | self.traverse_subpath.append(name) |
||
| 89 | return self |
||
| 90 | |||
| 91 | def fail(self, message, status=500, **kw): |
||
| 92 | """Set a JSON error object and a status to the response |
||
| 93 | """ |
||
| 94 | self.request.response.setStatus(status) |
||
| 95 | result = {"success": False, "errors": message, "status": status} |
||
| 96 | result.update(kw) |
||
| 97 | return result |
||
| 98 | |||
| 99 | @returns_json |
||
| 100 | def handle_ajax_request(self): |
||
| 101 | """Handle requests ajax routes |
||
| 102 | """ |
||
| 103 | # check if the method exists |
||
| 104 | func_arg = self.traverse_subpath[0] |
||
| 105 | func_name = "ajax_{}".format(func_arg) |
||
| 106 | func = getattr(self, func_name, None) |
||
| 107 | |||
| 108 | if func is None: |
||
| 109 | return self.fail("Invalid function", status=400) |
||
| 110 | |||
| 111 | # Additional provided path segments after the function name are handled |
||
| 112 | # as positional arguments |
||
| 113 | args = self.traverse_subpath[1:] |
||
| 114 | |||
| 115 | # check mandatory arguments |
||
| 116 | func_sig = inspect.getargspec(func) |
||
| 117 | # positional arguments after `self` argument |
||
| 118 | required_args = func_sig.args[1:] |
||
| 119 | |||
| 120 | if len(args) < len(required_args): |
||
| 121 | return self.fail("Wrong signature, please use '{}/{}'" |
||
| 122 | .format(func_arg, "/".join(required_args)), 400) |
||
| 123 | return func(*args) |
||
| 124 | |||
| 125 | def handle_http_request(self): |
||
| 126 | request = self.request |
||
| 127 | form = request.form |
||
| 128 | |||
| 129 | submitted = form.get("submitted", False) |
||
| 130 | send = form.get("send", False) |
||
| 131 | cancel = form.get("cancel", False) |
||
| 132 | |||
| 133 | if submitted and send: |
||
| 134 | logger.info("*** SENDING EMAIL ***") |
||
| 135 | |||
| 136 | # Parse used defined values from the request form |
||
| 137 | recipients = form.get("recipients", []) |
||
| 138 | responsibles = form.get("responsibles", []) |
||
| 139 | subject = form.get("subject") |
||
| 140 | body = form.get("body") |
||
| 141 | reports = self.get_reports() |
||
| 142 | |||
| 143 | # Merge recipiens and responsibles |
||
| 144 | recipients = set(recipients + responsibles) |
||
| 145 | |||
| 146 | # sanity checks |
||
| 147 | if not recipients: |
||
| 148 | message = _("No email recipients selected") |
||
| 149 | self.add_status_message(message, "error") |
||
| 150 | if not subject: |
||
| 151 | message = _("Please add an email subject") |
||
| 152 | self.add_status_message(message, "error") |
||
| 153 | if not body: |
||
| 154 | message = _("Please add an email text") |
||
| 155 | self.add_status_message(message, "error") |
||
| 156 | if not reports: |
||
| 157 | message = _("No attachments") |
||
| 158 | self.add_status_message(message, "error") |
||
| 159 | |||
| 160 | success = False |
||
| 161 | if all([recipients, subject, body, reports]): |
||
| 162 | attachments = [] |
||
| 163 | |||
| 164 | # report pdfs |
||
| 165 | for report in reports: |
||
| 166 | pdf = self.get_pdf(report) |
||
| 167 | if pdf is None: |
||
| 168 | logger.error("Skipping empty PDF for report {}" |
||
| 169 | .format(report.getId())) |
||
| 170 | continue |
||
| 171 | ar = report.getAnalysisRequest() |
||
| 172 | filename = "{}.pdf".format(ar.getId()) |
||
| 173 | filedata = pdf.data |
||
| 174 | attachments.append( |
||
| 175 | self.to_email_attachment(filename, filedata)) |
||
| 176 | |||
| 177 | # additional attachments |
||
| 178 | for attachment in self.get_attachments(): |
||
| 179 | af = attachment.getAttachmentFile() |
||
| 180 | filedata = af.data |
||
| 181 | filename = af.filename |
||
| 182 | attachments.append( |
||
| 183 | self.to_email_attachment(filename, filedata)) |
||
| 184 | |||
| 185 | success = self.send_email( |
||
| 186 | recipients, subject, body, attachments=attachments) |
||
| 187 | |||
| 188 | if success: |
||
| 189 | # selected name, email pairs which received the email |
||
| 190 | pairs = map(self.parse_email, recipients) |
||
| 191 | send_to_names = map(lambda p: p[0], pairs) |
||
| 192 | |||
| 193 | # set recipients to the reports |
||
| 194 | for report in reports: |
||
| 195 | ar = report.getAnalysisRequest() |
||
| 196 | # publish the AR |
||
| 197 | self.publish(ar) |
||
| 198 | |||
| 199 | # Publish all linked ARs of this report |
||
| 200 | # N.B. `ContainedAnalysisRequests` is an extended field |
||
| 201 | field = report.getField("ContainedAnalysisRequests") |
||
| 202 | contained_ars = field.get(report) or [] |
||
| 203 | for obj in contained_ars: |
||
| 204 | self.publish(obj) |
||
| 205 | |||
| 206 | # add new recipients to the AR Report |
||
| 207 | new_recipients = filter( |
||
| 208 | lambda r: r.get("Fullname") in send_to_names, |
||
|
|
|||
| 209 | self.get_recipients(ar)) |
||
| 210 | self.set_report_recipients(report, new_recipients) |
||
| 211 | |||
| 212 | message = _(u"Message sent to {}" |
||
| 213 | .format(", ".join(send_to_names))) |
||
| 214 | self.add_status_message(message, "info") |
||
| 215 | return request.response.redirect(self.exit_url) |
||
| 216 | else: |
||
| 217 | message = _("Failed to send Email(s)") |
||
| 218 | self.add_status_message(message, "error") |
||
| 219 | |||
| 220 | if submitted and cancel: |
||
| 221 | logger.info("*** EMAIL CANCELLED ***") |
||
| 222 | message = _("Email cancelled") |
||
| 223 | self.add_status_message(message, "info") |
||
| 224 | return request.response.redirect(self.exit_url) |
||
| 225 | |||
| 226 | # get the selected ARReport objects |
||
| 227 | reports = self.get_reports() |
||
| 228 | attachments = self.get_attachments() |
||
| 229 | |||
| 230 | # calculate the total size of all PDFs |
||
| 231 | self.total_size = self.get_total_size(reports, attachments) |
||
| 232 | if self.total_size > self.max_email_size: |
||
| 233 | # don't allow to send oversized emails |
||
| 234 | self.allow_send = False |
||
| 235 | message = _("Total size of email exceeded {:.1f} MB ({:.2f} MB)" |
||
| 236 | .format(self.max_email_size / 1024, |
||
| 237 | self.total_size / 1024)) |
||
| 238 | self.add_status_message(message, "error") |
||
| 239 | |||
| 240 | # prepare the data for the template |
||
| 241 | self.reports = map(self.get_report_data, reports) |
||
| 242 | self.recipients = self.get_recipients_data(reports) |
||
| 243 | self.responsibles = self.get_responsibles_data(reports) |
||
| 244 | |||
| 245 | # inform the user about invalid recipients |
||
| 246 | if not all(map(lambda r: r.get("valid"), self.recipients)): |
||
| 247 | message = _( |
||
| 248 | "Not all contacts are equal for the selected Reports. " |
||
| 249 | "Please manually select recipients for this email.") |
||
| 250 | self.add_status_message(message, "warning") |
||
| 251 | |||
| 252 | return self.template() |
||
| 253 | |||
| 254 | def set_report_recipients(self, report, recipients): |
||
| 255 | """Set recipients to the reports w/o overwriting the old ones |
||
| 256 | |||
| 257 | :param reports: list of ARReports |
||
| 258 | :param recipients: list of name,email strings |
||
| 259 | """ |
||
| 260 | to_set = report.getRecipients() |
||
| 261 | for recipient in recipients: |
||
| 262 | if recipient not in to_set: |
||
| 263 | to_set.append(recipient) |
||
| 264 | report.setRecipients(to_set) |
||
| 265 | |||
| 266 | def publish(self, ar): |
||
| 267 | """Set status to prepublished/published/republished |
||
| 268 | """ |
||
| 269 | wf = api.get_tool("portal_workflow") |
||
| 270 | status = wf.getInfoFor(ar, "review_state") |
||
| 271 | transitions = {"verified": "publish", |
||
| 272 | "published": "republish"} |
||
| 273 | transition = transitions.get(status, "prepublish") |
||
| 274 | logger.info("AR Transition: {} -> {}".format(status, transition)) |
||
| 275 | try: |
||
| 276 | wf.doActionFor(ar, transition) |
||
| 277 | return True |
||
| 278 | except WorkflowException as e: |
||
| 279 | logger.debug(e) |
||
| 280 | return False |
||
| 281 | |||
| 282 | def parse_email(self, email): |
||
| 283 | """parse an email to an unicode name, email tuple |
||
| 284 | """ |
||
| 285 | splitted = safe_unicode(email).rsplit(",", 1) |
||
| 286 | if len(splitted) == 1: |
||
| 287 | return (False, splitted[0]) |
||
| 288 | elif len(splitted) == 2: |
||
| 289 | return (splitted[0], splitted[1]) |
||
| 290 | else: |
||
| 291 | raise ValueError("Could not parse email '{}'".format(email)) |
||
| 292 | |||
| 293 | def to_email_attachment(self, filename, filedata, **kw): |
||
| 294 | """Create a new MIME Attachment |
||
| 295 | |||
| 296 | The Content-Type: header is build from the maintype and subtype of the |
||
| 297 | guessed filename mimetype. Additional parameters for this header are |
||
| 298 | taken from the keyword arguments. |
||
| 299 | """ |
||
| 300 | maintype = "application" |
||
| 301 | subtype = "octet-stream" |
||
| 302 | |||
| 303 | mime_type = mimetypes.guess_type(filename)[0] |
||
| 304 | if mime_type is not None: |
||
| 305 | maintype, subtype = mime_type.split("/") |
||
| 306 | |||
| 307 | attachment = MIMEBase(maintype, subtype, **kw) |
||
| 308 | attachment.set_payload(filedata) |
||
| 309 | encoders.encode_base64(attachment) |
||
| 310 | attachment.add_header("Content-Disposition", |
||
| 311 | "attachment; filename=%s" % filename) |
||
| 312 | return attachment |
||
| 313 | |||
| 314 | def send_email(self, recipients, subject, body, attachments=None): |
||
| 315 | """Prepare and send email to the recipients |
||
| 316 | |||
| 317 | :param recipients: a list of email or name,email strings |
||
| 318 | :param subject: the email subject |
||
| 319 | :param body: the email body |
||
| 320 | :param attachments: list of email attachments |
||
| 321 | :returns: True if all emails were sent, else false |
||
| 322 | """ |
||
| 323 | |||
| 324 | recipient_pairs = map(self.parse_email, recipients) |
||
| 325 | template_context = { |
||
| 326 | "recipients": "\n".join( |
||
| 327 | map(lambda p: formataddr(p), recipient_pairs)) |
||
| 328 | } |
||
| 329 | |||
| 330 | body_template = Template(safe_unicode(body)).safe_substitute( |
||
| 331 | **template_context) |
||
| 332 | |||
| 333 | _preamble = "This is a multi-part message in MIME format.\n" |
||
| 334 | _from = formataddr((self.email_from_name, self.email_from_address)) |
||
| 335 | _subject = Header(s=safe_unicode(subject), charset="utf8") |
||
| 336 | _body = MIMEText(body_template, _subtype="plain", _charset="utf8") |
||
| 337 | |||
| 338 | # Create the enclosing message |
||
| 339 | mime_msg = MIMEMultipart() |
||
| 340 | mime_msg.preamble = _preamble |
||
| 341 | mime_msg["Subject"] = _subject |
||
| 342 | mime_msg["From"] = _from |
||
| 343 | mime_msg.attach(_body) |
||
| 344 | |||
| 345 | # Attach attachments |
||
| 346 | for attachment in attachments: |
||
| 347 | mime_msg.attach(attachment) |
||
| 348 | |||
| 349 | success = [] |
||
| 350 | # Send one email per recipient |
||
| 351 | for pair in recipient_pairs: |
||
| 352 | # N.B.: Headers are added additive, so we need to remove any |
||
| 353 | # existing "To" headers |
||
| 354 | # No KeyError is raised if the key does not exist. |
||
| 355 | # https://docs.python.org/2/library/email.message.html#email.message.Message.__delitem__ |
||
| 356 | del mime_msg["To"] |
||
| 357 | |||
| 358 | # N.B. we use just the email here to prevent this Postfix Error: |
||
| 359 | # Recipient address rejected: User unknown in local recipient table |
||
| 360 | mime_msg["To"] = pair[1] |
||
| 361 | msg_string = mime_msg.as_string() |
||
| 362 | sent = self.send(msg_string) |
||
| 363 | if not sent: |
||
| 364 | logger.error("Could not send email to {}".format(pair)) |
||
| 365 | success.append(sent) |
||
| 366 | |||
| 367 | if not all(success): |
||
| 368 | return False |
||
| 369 | return True |
||
| 370 | |||
| 371 | def send(self, msg_string, immediate=True): |
||
| 372 | """Send the email via the MailHost tool |
||
| 373 | """ |
||
| 374 | try: |
||
| 375 | mailhost = api.get_tool("MailHost") |
||
| 376 | mailhost.send(msg_string, immediate=immediate) |
||
| 377 | except SMTPException as e: |
||
| 378 | logger.error(e) |
||
| 379 | return False |
||
| 380 | except socket.error as e: |
||
| 381 | logger.error(e) |
||
| 382 | return False |
||
| 383 | return True |
||
| 384 | |||
| 385 | def add_status_message(self, message, level="info"): |
||
| 386 | """Set a portal status message |
||
| 387 | """ |
||
| 388 | return self.context.plone_utils.addPortalMessage(message, level) |
||
| 389 | |||
| 390 | def get_report_data(self, report): |
||
| 391 | """Report data to be used in the template |
||
| 392 | """ |
||
| 393 | ar = report.getAnalysisRequest() |
||
| 394 | attachments = map(self.get_attachment_data, ar.getAttachment()) |
||
| 395 | pdf = self.get_pdf(report) |
||
| 396 | filesize = "{} Kb".format(self.get_filesize(pdf)) |
||
| 397 | filename = "{}.pdf".format(ar.getId()) |
||
| 398 | |||
| 399 | return { |
||
| 400 | "ar": ar, |
||
| 401 | "attachments": attachments, |
||
| 402 | "pdf": pdf, |
||
| 403 | "obj": report, |
||
| 404 | "uid": api.get_uid(report), |
||
| 405 | "filesize": filesize, |
||
| 406 | "filename": filename, |
||
| 407 | } |
||
| 408 | |||
| 409 | def get_attachment_data(self, attachment): |
||
| 410 | """Attachments data |
||
| 411 | """ |
||
| 412 | f = attachment.getAttachmentFile() |
||
| 413 | attachment_type = attachment.getAttachmentType() |
||
| 414 | attachment_keys = attachment.getAttachmentKeys() |
||
| 415 | filename = f.filename |
||
| 416 | filesize = self.get_filesize(f) |
||
| 417 | mimetype = f.getContentType() |
||
| 418 | report_option = attachment.getReportOption() |
||
| 419 | |||
| 420 | return { |
||
| 421 | "obj": attachment, |
||
| 422 | "attachment_type": attachment_type, |
||
| 423 | "attachment_keys": attachment_keys, |
||
| 424 | "file": f, |
||
| 425 | "uid": api.get_uid(attachment), |
||
| 426 | "filesize": filesize, |
||
| 427 | "filename": filename, |
||
| 428 | "mimetype": mimetype, |
||
| 429 | "report_option": report_option, |
||
| 430 | } |
||
| 431 | |||
| 432 | def get_recipients_data(self, reports): |
||
| 433 | """Recipients data to be used in the template |
||
| 434 | """ |
||
| 435 | if not reports: |
||
| 436 | return [] |
||
| 437 | |||
| 438 | recipients = [] |
||
| 439 | recipient_names = [] |
||
| 440 | |||
| 441 | for num, report in enumerate(reports): |
||
| 442 | # get the linked AR of this ARReport |
||
| 443 | ar = report.getAnalysisRequest() |
||
| 444 | # recipient names of this report |
||
| 445 | report_recipient_names = [] |
||
| 446 | for recipient in self.get_recipients(ar): |
||
| 447 | name = recipient.get("Fullname") |
||
| 448 | email = recipient.get("EmailAddress") |
||
| 449 | record = { |
||
| 450 | "name": name, |
||
| 451 | "email": email, |
||
| 452 | "valid": True, |
||
| 453 | } |
||
| 454 | if record not in recipients: |
||
| 455 | recipients.append(record) |
||
| 456 | # remember the name of the recipient for this report |
||
| 457 | report_recipient_names.append(name) |
||
| 458 | recipient_names.append(report_recipient_names) |
||
| 459 | |||
| 460 | # recipient names, which all of the reports have in common |
||
| 461 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
| 462 | # mark recipients not in common |
||
| 463 | for recipient in recipients: |
||
| 464 | if recipient.get("name") not in common_names: |
||
| 465 | recipient["valid"] = False |
||
| 466 | return recipients |
||
| 467 | |||
| 468 | def get_responsibles_data(self, reports): |
||
| 469 | """Responsibles data to be used in the template |
||
| 470 | """ |
||
| 471 | if not reports: |
||
| 472 | return [] |
||
| 473 | |||
| 474 | recipients = [] |
||
| 475 | recipient_names = [] |
||
| 476 | |||
| 477 | for num, report in enumerate(reports): |
||
| 478 | # get the linked AR of this ARReport |
||
| 479 | ar = report.getAnalysisRequest() |
||
| 480 | |||
| 481 | # recipient names of this report |
||
| 482 | report_recipient_names = [] |
||
| 483 | responsibles = ar.getResponsible() |
||
| 484 | for manager_id in responsibles.get("ids", []): |
||
| 485 | responsible = responsibles["dict"][manager_id] |
||
| 486 | name = responsible.get("name") |
||
| 487 | email = responsible.get("email") |
||
| 488 | record = { |
||
| 489 | "name": name, |
||
| 490 | "email": email, |
||
| 491 | "valid": True, |
||
| 492 | } |
||
| 493 | if record not in recipients: |
||
| 494 | recipients.append(record) |
||
| 495 | # remember the name of the recipient for this report |
||
| 496 | report_recipient_names.append(name) |
||
| 497 | recipient_names.append(report_recipient_names) |
||
| 498 | |||
| 499 | # recipient names, which all of the reports have in common |
||
| 500 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
| 501 | # mark recipients not in common |
||
| 502 | for recipient in recipients: |
||
| 503 | if recipient.get("name") not in common_names: |
||
| 504 | recipient["valid"] = False |
||
| 505 | |||
| 506 | return recipients |
||
| 507 | |||
| 508 | @property |
||
| 509 | def portal(self): |
||
| 510 | return api.get_portal() |
||
| 511 | |||
| 512 | @property |
||
| 513 | def laboratory(self): |
||
| 514 | return api.get_setup().laboratory |
||
| 515 | |||
| 516 | @property |
||
| 517 | def email_from_address(self): |
||
| 518 | """Portal email |
||
| 519 | """ |
||
| 520 | lab_email = self.laboratory.getEmailAddress() |
||
| 521 | portal_email = self.portal.email_from_address |
||
| 522 | return lab_email or portal_email |
||
| 523 | |||
| 524 | @property |
||
| 525 | def email_from_name(self): |
||
| 526 | """Portal email name |
||
| 527 | """ |
||
| 528 | lab_from_name = self.laboratory.getName() |
||
| 529 | portal_from_name = self.portal.email_from_name |
||
| 530 | return lab_from_name or portal_from_name |
||
| 531 | |||
| 532 | def get_total_size(self, *files): |
||
| 533 | """Calculate the total size of the given files |
||
| 534 | """ |
||
| 535 | |||
| 536 | # Recursive unpack an eventual list of lists |
||
| 537 | def iterate(item): |
||
| 538 | if isinstance(item, (list, tuple)): |
||
| 539 | for i in item: |
||
| 540 | for ii in iterate(i): |
||
| 541 | yield ii |
||
| 542 | else: |
||
| 543 | yield item |
||
| 544 | |||
| 545 | # Calculate the total size of the given objects starting with an |
||
| 546 | # initial size of 0 |
||
| 547 | return reduce(lambda x, y: x + y, |
||
| 548 | map(self.get_filesize, iterate(files)), 0) |
||
| 549 | |||
| 550 | @property |
||
| 551 | def max_email_size(self): |
||
| 552 | """Return the max. allowed email size in KB |
||
| 553 | """ |
||
| 554 | # TODO: Refactor to customizable setup option |
||
| 555 | max_size = EMAIL_MAX_SIZE |
||
| 556 | if max_size < 0: |
||
| 557 | return 0.0 |
||
| 558 | return max_size * 1024 |
||
| 559 | |||
| 560 | def get_reports(self): |
||
| 561 | """Return the objects from the UIDs given in the request |
||
| 562 | """ |
||
| 563 | # Create a mapping of source ARs for copy |
||
| 564 | uids = self.request.form.get("uids", []) |
||
| 565 | # handle 'uids' GET parameter coming from a redirect |
||
| 566 | if isinstance(uids, basestring): |
||
| 567 | uids = uids.split(",") |
||
| 568 | uids = filter(api.is_uid, uids) |
||
| 569 | unique_uids = OrderedDict().fromkeys(uids).keys() |
||
| 570 | return map(self.get_object_by_uid, unique_uids) |
||
| 571 | |||
| 572 | def get_attachments(self): |
||
| 573 | """Return the objects from the UIDs given in the request |
||
| 574 | """ |
||
| 575 | # Create a mapping of source ARs for copy |
||
| 576 | uids = self.request.form.get("attachment_uids", []) |
||
| 577 | return map(self.get_object_by_uid, uids) |
||
| 578 | |||
| 579 | def get_object_by_uid(self, uid): |
||
| 580 | """Get the object by UID |
||
| 581 | """ |
||
| 582 | logger.debug("get_object_by_uid::UID={}".format(uid)) |
||
| 583 | obj = api.get_object_by_uid(uid, None) |
||
| 584 | if obj is None: |
||
| 585 | logger.warn("!! No object found for UID #{} !!") |
||
| 586 | return obj |
||
| 587 | |||
| 588 | def get_filesize(self, f): |
||
| 589 | """Return the filesize of the PDF as a float |
||
| 590 | """ |
||
| 591 | try: |
||
| 592 | filesize = float(f.get_size()) |
||
| 593 | return float("%.2f" % (filesize / 1024)) |
||
| 594 | except (POSKeyError, TypeError, AttributeError): |
||
| 595 | return 0.0 |
||
| 596 | |||
| 597 | def get_pdf(self, obj): |
||
| 598 | """Get the report PDF |
||
| 599 | """ |
||
| 600 | try: |
||
| 601 | return obj.getPdf() |
||
| 602 | except (POSKeyError, TypeError): |
||
| 603 | return None |
||
| 604 | |||
| 605 | View Code Duplication | def get_recipients(self, ar): |
|
| 606 | """Return the AR recipients in the same format like the AR Report |
||
| 607 | expects in the records field `Recipients` |
||
| 608 | """ |
||
| 609 | plone_utils = api.get_tool("plone_utils") |
||
| 610 | |||
| 611 | def is_email(email): |
||
| 612 | if not plone_utils.validateSingleEmailAddress(email): |
||
| 613 | return False |
||
| 614 | return True |
||
| 615 | |||
| 616 | def recipient_from_contact(contact): |
||
| 617 | if not contact: |
||
| 618 | return None |
||
| 619 | email = contact.getEmailAddress() |
||
| 620 | return { |
||
| 621 | "UID": api.get_uid(contact), |
||
| 622 | "Username": contact.getUsername(), |
||
| 623 | "Fullname": to_utf8(contact.Title()), |
||
| 624 | "EmailAddress": email, |
||
| 625 | } |
||
| 626 | |||
| 627 | def recipient_from_email(email): |
||
| 628 | if not is_email(email): |
||
| 629 | return None |
||
| 630 | return { |
||
| 631 | "UID": "", |
||
| 632 | "Username": "", |
||
| 633 | "Fullname": email, |
||
| 634 | "EmailAddress": email, |
||
| 635 | } |
||
| 636 | |||
| 637 | # Primary Contacts |
||
| 638 | to = filter(None, [recipient_from_contact(ar.getContact())]) |
||
| 639 | # CC Contacts |
||
| 640 | cc = filter(None, map(recipient_from_contact, ar.getCCContact())) |
||
| 641 | # CC Emails |
||
| 642 | cc_emails = map(lambda x: x.strip(), ar.getCCEmails().split(",")) |
||
| 643 | cc_emails = filter(None, map(recipient_from_email, cc_emails)) |
||
| 644 | |||
| 645 | return to + cc + cc_emails |
||
| 646 | |||
| 647 | def ajax_recalculate_size(self): |
||
| 648 | """Recalculate the total size of the selected attachments |
||
| 649 | """ |
||
| 650 | reports = self.get_reports() |
||
| 651 | attachments = self.get_attachments() |
||
| 652 | total_size = self.get_total_size(reports, attachments) |
||
| 653 | |||
| 654 | return { |
||
| 655 | "files": len(reports) + len(attachments), |
||
| 656 | "size": "%.2f" % total_size, |
||
| 657 | "limit": self.max_email_size, |
||
| 658 | "limit_exceeded": total_size > self.max_email_size, |
||
| 659 | } |
||
| 660 |