| Total Complexity | 106 |
| Total Lines | 805 |
| Duplicated Lines | 5.09 % |
| 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-2021 by it's authors. |
||
| 19 | # Some rights reserved, see README and LICENSE. |
||
| 20 | |||
| 21 | import functools |
||
| 22 | import inspect |
||
| 23 | import itertools |
||
| 24 | from collections import OrderedDict |
||
| 25 | from string import Template |
||
| 26 | |||
| 27 | import six |
||
| 28 | |||
| 29 | import transaction |
||
| 30 | from bika.lims import _ |
||
| 31 | from bika.lims import api |
||
| 32 | from bika.lims import logger |
||
| 33 | from bika.lims.api import mail as mailapi |
||
| 34 | from bika.lims.api.security import get_user |
||
| 35 | from bika.lims.api.security import get_user_id |
||
| 36 | from bika.lims.api.snapshot import take_snapshot |
||
| 37 | from bika.lims.decorators import returns_json |
||
| 38 | from bika.lims.interfaces import IAnalysisRequest |
||
| 39 | from bika.lims.utils import to_utf8 |
||
| 40 | from DateTime import DateTime |
||
| 41 | from plone.memoize import view |
||
| 42 | from Products.CMFCore.WorkflowCore import WorkflowException |
||
| 43 | from Products.CMFPlone.utils import safe_unicode |
||
| 44 | from Products.Five.browser import BrowserView |
||
| 45 | from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
||
| 46 | from ZODB.POSException import POSKeyError |
||
| 47 | from zope.interface import implements |
||
| 48 | from zope.publisher.interfaces import IPublishTraverse |
||
| 49 | |||
| 50 | DEFAULT_MAX_EMAIL_SIZE = 15 |
||
| 51 | |||
| 52 | |||
| 53 | class EmailView(BrowserView): |
||
| 54 | """Email Attachments View |
||
| 55 | """ |
||
| 56 | implements(IPublishTraverse) |
||
| 57 | |||
| 58 | template = ViewPageTemplateFile("templates/email.pt") |
||
| 59 | |||
| 60 | def __init__(self, context, request): |
||
| 61 | super(EmailView, self).__init__(context, request) |
||
| 62 | # disable Plone's editable border |
||
| 63 | request.set("disable_border", True) |
||
| 64 | # list of requested subpaths |
||
| 65 | self.traverse_subpath = [] |
||
| 66 | # toggle to allow email sending |
||
| 67 | self.allow_send = True |
||
| 68 | |||
| 69 | def __call__(self): |
||
| 70 | # dispatch subpath request to `ajax_` methods |
||
| 71 | if len(self.traverse_subpath) > 0: |
||
| 72 | return self.handle_ajax_request() |
||
| 73 | |||
| 74 | # handle standard request |
||
| 75 | form = self.request.form |
||
| 76 | send = form.get("send", False) and True or False |
||
| 77 | cancel = form.get("cancel", False) and True or False |
||
| 78 | |||
| 79 | if send and self.validate_email_form(): |
||
| 80 | logger.info("*** PUBLISH SAMPLES & SEND REPORTS ***") |
||
| 81 | # 1. Publish all samples |
||
| 82 | self.publish_samples() |
||
| 83 | # 2. Notify all recipients |
||
| 84 | self.form_action_send() |
||
| 85 | |||
| 86 | elif cancel: |
||
| 87 | logger.info("*** CANCEL EMAIL PUBLICATION ***") |
||
| 88 | self.form_action_cancel() |
||
| 89 | |||
| 90 | else: |
||
| 91 | logger.info("*** RENDER EMAIL FORM ***") |
||
| 92 | # validate email size |
||
| 93 | self.validate_email_size() |
||
| 94 | # validate email recipients |
||
| 95 | self.validate_email_recipients() |
||
| 96 | |||
| 97 | return self.template() |
||
| 98 | |||
| 99 | def publishTraverse(self, request, name): |
||
| 100 | """Called before __call__ for each path name |
||
| 101 | |||
| 102 | Appends the path to the additional requested path after the view name |
||
| 103 | to the internal `traverse_subpath` list |
||
| 104 | """ |
||
| 105 | self.traverse_subpath.append(name) |
||
| 106 | return self |
||
| 107 | |||
| 108 | @returns_json |
||
| 109 | def handle_ajax_request(self): |
||
| 110 | """Handle requests ajax routes |
||
| 111 | """ |
||
| 112 | # check if the method exists |
||
| 113 | func_arg = self.traverse_subpath[0] |
||
| 114 | func_name = "ajax_{}".format(func_arg) |
||
| 115 | func = getattr(self, func_name, None) |
||
| 116 | |||
| 117 | if func is None: |
||
| 118 | return self.fail("Invalid function", status=400) |
||
| 119 | |||
| 120 | # Additional provided path segments after the function name are handled |
||
| 121 | # as positional arguments |
||
| 122 | args = self.traverse_subpath[1:] |
||
| 123 | |||
| 124 | # check mandatory arguments |
||
| 125 | func_sig = inspect.getargspec(func) |
||
| 126 | # positional arguments after `self` argument |
||
| 127 | required_args = func_sig.args[1:] |
||
| 128 | |||
| 129 | if len(args) < len(required_args): |
||
| 130 | return self.fail("Wrong signature, please use '{}/{}'" |
||
| 131 | .format(func_arg, "/".join(required_args)), 400) |
||
| 132 | return func(*args) |
||
| 133 | |||
| 134 | def form_action_send(self): |
||
| 135 | """Send form handler |
||
| 136 | """ |
||
| 137 | # send email to the selected recipients and responsibles |
||
| 138 | success = self.send_email(self.email_recipients_and_responsibles, |
||
| 139 | self.email_subject, |
||
| 140 | self.email_body, |
||
| 141 | attachments=self.email_attachments) |
||
| 142 | |||
| 143 | if success: |
||
| 144 | # write email sendlog log to keep track of the email submission |
||
| 145 | self.write_sendlog() |
||
| 146 | message = _(u"Message sent to {}".format( |
||
| 147 | ", ".join(self.email_recipients_and_responsibles))) |
||
| 148 | self.add_status_message(message, "info") |
||
| 149 | else: |
||
| 150 | message = _("Failed to send Email(s)") |
||
| 151 | self.add_status_message(message, "error") |
||
| 152 | |||
| 153 | self.request.response.redirect(self.exit_url) |
||
| 154 | |||
| 155 | def form_action_cancel(self): |
||
| 156 | """Cancel form handler |
||
| 157 | """ |
||
| 158 | self.add_status_message(_("Email cancelled"), "info") |
||
| 159 | self.request.response.redirect(self.exit_url) |
||
| 160 | |||
| 161 | def validate_email_form(self): |
||
| 162 | """Validate if the email form is complete for send |
||
| 163 | |||
| 164 | :returns: True if the validator passed, otherwise False |
||
| 165 | """ |
||
| 166 | if not self.email_recipients_and_responsibles: |
||
| 167 | message = _("No email recipients selected") |
||
| 168 | self.add_status_message(message, "error") |
||
| 169 | if not self.email_subject: |
||
| 170 | message = _("Please add an email subject") |
||
| 171 | self.add_status_message(message, "error") |
||
| 172 | if not self.email_body: |
||
| 173 | message = _("Please add an email text") |
||
| 174 | self.add_status_message(message, "error") |
||
| 175 | if not self.reports: |
||
| 176 | message = _("No reports found") |
||
| 177 | self.add_status_message(message, "error") |
||
| 178 | |||
| 179 | if not all([self.email_recipients_and_responsibles, |
||
| 180 | self.email_subject, |
||
| 181 | self.email_body, |
||
| 182 | self.reports]): |
||
| 183 | return False |
||
| 184 | return True |
||
| 185 | |||
| 186 | def validate_email_size(self): |
||
| 187 | """Validate if the email size exceeded the max. allowed size |
||
| 188 | |||
| 189 | :returns: True if the validator passed, otherwise False |
||
| 190 | """ |
||
| 191 | if self.total_size > self.max_email_size: |
||
| 192 | # don't allow to send oversized emails |
||
| 193 | self.allow_send = False |
||
| 194 | message = _("Total size of email exceeded {:.1f} MB ({:.2f} MB)" |
||
| 195 | .format(self.max_email_size / 1024, |
||
| 196 | self.total_size / 1024)) |
||
| 197 | self.add_status_message(message, "error") |
||
| 198 | return False |
||
| 199 | return True |
||
| 200 | |||
| 201 | def validate_email_recipients(self): |
||
| 202 | """Validate if the recipients are all valid |
||
| 203 | |||
| 204 | :returns: True if the validator passed, otherwise False |
||
| 205 | """ |
||
| 206 | # inform the user about invalid recipients |
||
| 207 | if not all(map(lambda r: r.get("valid"), self.recipients_data)): |
||
| 208 | message = _( |
||
| 209 | "Not all contacts are equal for the selected Reports. " |
||
| 210 | "Please manually select recipients for this email.") |
||
| 211 | self.add_status_message(message, "warning") |
||
| 212 | return False |
||
| 213 | return True |
||
| 214 | |||
| 215 | @property |
||
| 216 | def portal(self): |
||
| 217 | """Get the portal object |
||
| 218 | """ |
||
| 219 | return api.get_portal() |
||
| 220 | |||
| 221 | @property |
||
| 222 | def setup(self): |
||
| 223 | """Get the setup object |
||
| 224 | """ |
||
| 225 | return api.get_setup() |
||
| 226 | |||
| 227 | @property |
||
| 228 | def laboratory(self): |
||
| 229 | """Laboratory object from the LIMS setup |
||
| 230 | """ |
||
| 231 | return api.get_setup().laboratory |
||
| 232 | |||
| 233 | @property |
||
| 234 | def always_cc_responsibles(self): |
||
| 235 | """Setting if responsibles should be always CC'ed |
||
| 236 | """ |
||
| 237 | return self.setup.getAlwaysCCResponsiblesInReportEmail() |
||
| 238 | |||
| 239 | @property |
||
| 240 | @view.memoize |
||
| 241 | def reports(self): |
||
| 242 | """Return the objects from the UIDs given in the request |
||
| 243 | """ |
||
| 244 | # Create a mapping of source ARs for copy |
||
| 245 | uids = self.request.form.get("uids", []) |
||
| 246 | # handle 'uids' GET parameter coming from a redirect |
||
| 247 | if isinstance(uids, six.string_types): |
||
| 248 | uids = uids.split(",") |
||
| 249 | uids = filter(api.is_uid, uids) |
||
| 250 | unique_uids = OrderedDict().fromkeys(uids).keys() |
||
| 251 | return map(self.get_object_by_uid, unique_uids) |
||
| 252 | |||
| 253 | @property |
||
| 254 | @view.memoize |
||
| 255 | def attachments(self): |
||
| 256 | """Return the objects from the UIDs given in the request |
||
| 257 | """ |
||
| 258 | uids = self.request.form.get("attachment_uids", []) |
||
| 259 | return map(self.get_object_by_uid, uids) |
||
| 260 | |||
| 261 | @property |
||
| 262 | def email_sender_address(self): |
||
| 263 | """Sender email is either the lab email or portal email "from" address |
||
| 264 | """ |
||
| 265 | lab_email = self.laboratory.getEmailAddress() |
||
| 266 | portal_email = api.get_registry_record("plone.email_from_address") |
||
| 267 | return lab_email or portal_email or "" |
||
| 268 | |||
| 269 | @property |
||
| 270 | def email_sender_name(self): |
||
| 271 | """Sender name is either the lab name or the portal email "from" name |
||
| 272 | """ |
||
| 273 | lab_from_name = self.laboratory.getName() |
||
| 274 | portal_from_name = api.get_registry_record("plone.email_from_name") |
||
| 275 | return lab_from_name or portal_from_name |
||
| 276 | |||
| 277 | @property |
||
| 278 | def email_recipients_and_responsibles(self): |
||
| 279 | """Returns a unified list of recipients and responsibles |
||
| 280 | """ |
||
| 281 | return list(set(self.email_recipients + self.email_responsibles)) |
||
| 282 | |||
| 283 | @property |
||
| 284 | def email_recipients(self): |
||
| 285 | """Email addresses of the selected recipients |
||
| 286 | """ |
||
| 287 | return map(safe_unicode, self.request.form.get("recipients", [])) |
||
| 288 | |||
| 289 | @property |
||
| 290 | def email_responsibles(self): |
||
| 291 | """Email addresses of the responsible persons |
||
| 292 | """ |
||
| 293 | return map(safe_unicode, self.request.form.get("responsibles", [])) |
||
| 294 | |||
| 295 | @property |
||
| 296 | def email_subject(self): |
||
| 297 | """Email subject line to be used in the template |
||
| 298 | """ |
||
| 299 | # request parameter has precedence |
||
| 300 | subject = self.request.get("subject", None) |
||
| 301 | if subject is not None: |
||
| 302 | return subject |
||
| 303 | subject = self.context.translate(_("Analysis Results for {}")) |
||
| 304 | return subject.format(self.client_name) |
||
| 305 | |||
| 306 | @property |
||
| 307 | def email_body(self): |
||
| 308 | """Email body text to be used in the template |
||
| 309 | """ |
||
| 310 | # request parameter has precedence |
||
| 311 | body = self.request.get("body", None) |
||
| 312 | if body is not None: |
||
| 313 | return body |
||
| 314 | setup = api.get_setup() |
||
| 315 | body = setup.getEmailBodySamplePublication() |
||
| 316 | template_context = { |
||
| 317 | "client_name": self.client_name, |
||
| 318 | "lab_name": self.lab_name, |
||
| 319 | "lab_address": self.lab_address, |
||
| 320 | } |
||
| 321 | rendered_body = self.render_email_template( |
||
| 322 | body, template_context=template_context) |
||
| 323 | return rendered_body |
||
| 324 | |||
| 325 | @property |
||
| 326 | def email_attachments(self): |
||
| 327 | attachments = [] |
||
| 328 | |||
| 329 | # Convert report PDFs -> email attachments |
||
| 330 | for report in self.reports: |
||
| 331 | pdf = self.get_pdf(report) |
||
| 332 | if pdf is None: |
||
| 333 | logger.error("Skipping empty PDF for report {}" |
||
| 334 | .format(report.getId())) |
||
| 335 | continue |
||
| 336 | filename = self.get_report_filename(report) |
||
| 337 | filedata = pdf.data |
||
| 338 | attachments.append( |
||
| 339 | mailapi.to_email_attachment(filedata, filename)) |
||
| 340 | |||
| 341 | # Convert additional attachments |
||
| 342 | for attachment in self.attachments: |
||
| 343 | af = attachment.getAttachmentFile() |
||
| 344 | filedata = af.data |
||
| 345 | filename = af.filename |
||
| 346 | attachments.append( |
||
| 347 | mailapi.to_email_attachment(filedata, filename)) |
||
| 348 | |||
| 349 | return attachments |
||
| 350 | |||
| 351 | @property |
||
| 352 | def reports_data(self): |
||
| 353 | """Returns a list of report data dictionaries |
||
| 354 | """ |
||
| 355 | reports = self.reports |
||
| 356 | return map(self.get_report_data, reports) |
||
| 357 | |||
| 358 | @property |
||
| 359 | def recipients_data(self): |
||
| 360 | """Returns a list of recipients data dictionaries |
||
| 361 | """ |
||
| 362 | reports = self.reports |
||
| 363 | return self.get_recipients_data(reports) |
||
| 364 | |||
| 365 | @property |
||
| 366 | def responsibles_data(self): |
||
| 367 | """Returns a list of responsibles data dictionaries |
||
| 368 | """ |
||
| 369 | reports = self.reports |
||
| 370 | return self.get_responsibles_data(reports) |
||
| 371 | |||
| 372 | @property |
||
| 373 | def client_name(self): |
||
| 374 | """Returns the client name |
||
| 375 | """ |
||
| 376 | return safe_unicode(self.context.Title()) |
||
| 377 | |||
| 378 | @property |
||
| 379 | def lab_address(self): |
||
| 380 | """Returns the laboratory print address |
||
| 381 | """ |
||
| 382 | return "<br/>".join(self.laboratory.getPrintAddress()) |
||
| 383 | |||
| 384 | @property |
||
| 385 | def lab_name(self): |
||
| 386 | """Returns the laboratory name |
||
| 387 | """ |
||
| 388 | return self.laboratory.getName() |
||
| 389 | |||
| 390 | @property |
||
| 391 | def exit_url(self): |
||
| 392 | """Exit URL for redirect |
||
| 393 | """ |
||
| 394 | endpoint = "reports_listing" |
||
| 395 | if IAnalysisRequest.providedBy(self.context): |
||
| 396 | endpoint = "published_results" |
||
| 397 | return "{}/{}".format( |
||
| 398 | api.get_url(self.context), endpoint) |
||
| 399 | |||
| 400 | @property |
||
| 401 | def total_size(self): |
||
| 402 | """Total size of all report PDFs + additional attachments |
||
| 403 | """ |
||
| 404 | reports = self.reports |
||
| 405 | attachments = self.attachments |
||
| 406 | return self.get_total_size(reports, attachments) |
||
| 407 | |||
| 408 | @property |
||
| 409 | def max_email_size(self): |
||
| 410 | """Return the max. allowed email size in KB |
||
| 411 | """ |
||
| 412 | # check first if a registry record exists |
||
| 413 | max_email_size = api.get_registry_record( |
||
| 414 | "senaite.core.max_email_size") |
||
| 415 | if max_email_size is None: |
||
| 416 | max_size = DEFAULT_MAX_EMAIL_SIZE |
||
| 417 | if max_size < 0: |
||
|
|
|||
| 418 | max_email_size = 0 |
||
| 419 | return max_size * 1024 |
||
| 420 | |||
| 421 | def make_sendlog_record(self, **kw): |
||
| 422 | """Create a new sendlog record |
||
| 423 | """ |
||
| 424 | user = get_user() |
||
| 425 | actor = get_user_id() |
||
| 426 | userprops = api.get_user_properties(user) |
||
| 427 | actor_fullname = userprops.get("fullname", actor) |
||
| 428 | email_send_date = DateTime() |
||
| 429 | email_recipients = self.email_recipients |
||
| 430 | email_responsibles = self.email_responsibles |
||
| 431 | email_subject = self.email_subject |
||
| 432 | email_body = self.render_email_template(self.email_body) |
||
| 433 | email_attachments = map(api.get_uid, self.attachments) |
||
| 434 | |||
| 435 | record = { |
||
| 436 | "actor": actor, |
||
| 437 | "actor_fullname": actor_fullname, |
||
| 438 | "email_send_date": email_send_date, |
||
| 439 | "email_recipients": email_recipients, |
||
| 440 | "email_responsibles": email_responsibles, |
||
| 441 | "email_subject": email_subject, |
||
| 442 | "email_body": email_body, |
||
| 443 | "email_attachments": email_attachments, |
||
| 444 | |||
| 445 | } |
||
| 446 | # keywords take precedence |
||
| 447 | record.update(kw) |
||
| 448 | return record |
||
| 449 | |||
| 450 | def write_sendlog(self): |
||
| 451 | """Write email sendlog |
||
| 452 | """ |
||
| 453 | timestamp = DateTime() |
||
| 454 | |||
| 455 | for report in self.reports: |
||
| 456 | # get the current sendlog records |
||
| 457 | records = report.getSendLog() |
||
| 458 | # create a new record with the current data |
||
| 459 | new_record = self.make_sendlog_record(email_send_date=timestamp) |
||
| 460 | # set the new record to the existing records |
||
| 461 | records.append(new_record) |
||
| 462 | report.setSendLog(records) |
||
| 463 | # reindex object to make changes visible in the snapshot |
||
| 464 | report.reindexObject() |
||
| 465 | # manually take a new snapshot |
||
| 466 | take_snapshot(report) |
||
| 467 | |||
| 468 | def publish_samples(self): |
||
| 469 | """Publish all samples of the reports |
||
| 470 | """ |
||
| 471 | samples = set() |
||
| 472 | |||
| 473 | # collect primary + contained samples of the reports |
||
| 474 | for report in self.reports: |
||
| 475 | samples.add(report.getAnalysisRequest()) |
||
| 476 | samples.update(report.getContainedAnalysisRequests()) |
||
| 477 | |||
| 478 | # publish all samples + their partitions |
||
| 479 | for sample in samples: |
||
| 480 | self.publish(sample) |
||
| 481 | |||
| 482 | def publish(self, sample): |
||
| 483 | """Set status to prepublished/published/republished |
||
| 484 | """ |
||
| 485 | wf = api.get_tool("portal_workflow") |
||
| 486 | status = wf.getInfoFor(sample, "review_state") |
||
| 487 | transitions = {"verified": "publish", |
||
| 488 | "published": "republish"} |
||
| 489 | transition = transitions.get(status, "prepublish") |
||
| 490 | logger.info("Transitioning sample {}: {} -> {}".format( |
||
| 491 | api.get_id(sample), status, transition)) |
||
| 492 | try: |
||
| 493 | # Manually update the view on the database to avoid conflict errors |
||
| 494 | sample.getClient()._p_jar.sync() |
||
| 495 | # Perform WF transition |
||
| 496 | wf.doActionFor(sample, transition) |
||
| 497 | # Commit the changes |
||
| 498 | transaction.commit() |
||
| 499 | except WorkflowException as e: |
||
| 500 | logger.error(e) |
||
| 501 | |||
| 502 | def render_email_template(self, template, template_context=None): |
||
| 503 | """Return the rendered email template |
||
| 504 | |||
| 505 | This method interpolates the $recipients variable with the selected |
||
| 506 | recipients from the email form. |
||
| 507 | |||
| 508 | :params template: Email body text |
||
| 509 | :returns: Rendered email template |
||
| 510 | """ |
||
| 511 | |||
| 512 | # allow to add translation for initial template |
||
| 513 | template = self.context.translate(_(template)) |
||
| 514 | recipients = self.email_recipients_and_responsibles |
||
| 515 | if template_context is None: |
||
| 516 | template_context = { |
||
| 517 | "recipients": "<br/>".join(recipients), |
||
| 518 | } |
||
| 519 | |||
| 520 | email_template = Template(safe_unicode(template)).safe_substitute( |
||
| 521 | **template_context) |
||
| 522 | |||
| 523 | return email_template |
||
| 524 | |||
| 525 | def send_email(self, recipients, subject, body, attachments=None): |
||
| 526 | """Prepare and send email to the recipients |
||
| 527 | |||
| 528 | :param recipients: a list of email or name,email strings |
||
| 529 | :param subject: the email subject |
||
| 530 | :param body: the email body |
||
| 531 | :param attachments: list of email attachments |
||
| 532 | :returns: True if all emails were sent, else False |
||
| 533 | """ |
||
| 534 | email_body = self.render_email_template(body) |
||
| 535 | |||
| 536 | success = [] |
||
| 537 | # Send one email per recipient |
||
| 538 | for recipient in recipients: |
||
| 539 | # N.B. we use just the email here to prevent this Postfix Error: |
||
| 540 | # Recipient address rejected: User unknown in local recipient table |
||
| 541 | pair = mailapi.parse_email_address(recipient) |
||
| 542 | to_address = pair[1] |
||
| 543 | mime_msg = mailapi.compose_email(self.email_sender_address, |
||
| 544 | to_address, |
||
| 545 | subject, |
||
| 546 | email_body, |
||
| 547 | attachments=attachments, |
||
| 548 | html=True) |
||
| 549 | sent = mailapi.send_email(mime_msg) |
||
| 550 | if not sent: |
||
| 551 | msg = _("Could not send email to {0} ({1})").format(pair[0], |
||
| 552 | pair[1]) |
||
| 553 | self.add_status_message(msg, "warning") |
||
| 554 | logger.error(msg) |
||
| 555 | success.append(sent) |
||
| 556 | |||
| 557 | if not all(success): |
||
| 558 | return False |
||
| 559 | return True |
||
| 560 | |||
| 561 | def add_status_message(self, message, level="info"): |
||
| 562 | """Set a portal status message |
||
| 563 | """ |
||
| 564 | return self.context.plone_utils.addPortalMessage(message, level) |
||
| 565 | |||
| 566 | def get_report_data(self, report): |
||
| 567 | """Report data to be used in the template |
||
| 568 | """ |
||
| 569 | sample = report.getAnalysisRequest() |
||
| 570 | analyses = sample.getAnalyses(full_objects=True) |
||
| 571 | # merge together sample + analyses attachments |
||
| 572 | attachments = itertools.chain( |
||
| 573 | sample.getAttachment(), |
||
| 574 | *map(lambda an: an.getAttachment(), analyses)) |
||
| 575 | attachments_data = map(self.get_attachment_data, attachments) |
||
| 576 | pdf = self.get_pdf(report) |
||
| 577 | filesize = "{} Kb".format(self.get_filesize(pdf)) |
||
| 578 | filename = self.get_report_filename(report) |
||
| 579 | |||
| 580 | return { |
||
| 581 | "sample": sample, |
||
| 582 | "attachments": attachments_data, |
||
| 583 | "pdf": pdf, |
||
| 584 | "obj": report, |
||
| 585 | "uid": api.get_uid(report), |
||
| 586 | "filesize": filesize, |
||
| 587 | "filename": filename, |
||
| 588 | } |
||
| 589 | |||
| 590 | def get_attachment_data(self, attachment): |
||
| 591 | """Attachments data to be used in the template |
||
| 592 | """ |
||
| 593 | f = attachment.getAttachmentFile() |
||
| 594 | attachment_type = attachment.getAttachmentType() |
||
| 595 | attachment_keys = attachment.getAttachmentKeys() |
||
| 596 | filename = f.filename |
||
| 597 | filesize = self.get_filesize(f) |
||
| 598 | mimetype = f.getContentType() |
||
| 599 | report_option = attachment.getReportOption() |
||
| 600 | |||
| 601 | return { |
||
| 602 | "obj": attachment, |
||
| 603 | "attachment_type": attachment_type, |
||
| 604 | "attachment_keys": attachment_keys, |
||
| 605 | "file": f, |
||
| 606 | "uid": api.get_uid(attachment), |
||
| 607 | "filesize": filesize, |
||
| 608 | "filename": filename, |
||
| 609 | "mimetype": mimetype, |
||
| 610 | "report_option": report_option, |
||
| 611 | } |
||
| 612 | |||
| 613 | def get_recipients_data(self, reports): |
||
| 614 | """Recipients data to be used in the template |
||
| 615 | """ |
||
| 616 | if not reports: |
||
| 617 | return [] |
||
| 618 | |||
| 619 | recipients = [] |
||
| 620 | recipient_names = [] |
||
| 621 | |||
| 622 | for num, report in enumerate(reports): |
||
| 623 | sample = report.getAnalysisRequest() |
||
| 624 | # recipient names of this report |
||
| 625 | report_recipient_names = [] |
||
| 626 | for recipient in self.get_recipients(sample): |
||
| 627 | name = recipient.get("Fullname") |
||
| 628 | email = recipient.get("EmailAddress") |
||
| 629 | address = mailapi.to_email_address(email, name=name) |
||
| 630 | record = { |
||
| 631 | "name": name, |
||
| 632 | "email": email, |
||
| 633 | "address": address, |
||
| 634 | "valid": True, |
||
| 635 | } |
||
| 636 | if record not in recipients: |
||
| 637 | recipients.append(record) |
||
| 638 | # remember the name of the recipient for this report |
||
| 639 | report_recipient_names.append(name) |
||
| 640 | recipient_names.append(report_recipient_names) |
||
| 641 | |||
| 642 | # recipient names, which all of the reports have in common |
||
| 643 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
| 644 | # mark recipients not in common |
||
| 645 | for recipient in recipients: |
||
| 646 | if recipient.get("name") not in common_names: |
||
| 647 | recipient["valid"] = False |
||
| 648 | return recipients |
||
| 649 | |||
| 650 | def get_responsibles_data(self, reports): |
||
| 651 | """Responsibles data to be used in the template |
||
| 652 | """ |
||
| 653 | if not reports: |
||
| 654 | return [] |
||
| 655 | |||
| 656 | recipients = [] |
||
| 657 | recipient_names = [] |
||
| 658 | |||
| 659 | for num, report in enumerate(reports): |
||
| 660 | # get the linked AR of this ARReport |
||
| 661 | ar = report.getAnalysisRequest() |
||
| 662 | |||
| 663 | # recipient names of this report |
||
| 664 | report_recipient_names = [] |
||
| 665 | responsibles = ar.getResponsible() |
||
| 666 | for manager_id in responsibles.get("ids", []): |
||
| 667 | responsible = responsibles["dict"][manager_id] |
||
| 668 | name = responsible.get("name") |
||
| 669 | email = responsible.get("email") |
||
| 670 | address = mailapi.to_email_address(email, name=name) |
||
| 671 | record = { |
||
| 672 | "name": name, |
||
| 673 | "email": email, |
||
| 674 | "address": address, |
||
| 675 | "valid": True, |
||
| 676 | } |
||
| 677 | if record not in recipients: |
||
| 678 | recipients.append(record) |
||
| 679 | # remember the name of the recipient for this report |
||
| 680 | report_recipient_names.append(name) |
||
| 681 | recipient_names.append(report_recipient_names) |
||
| 682 | |||
| 683 | # recipient names, which all of the reports have in common |
||
| 684 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
| 685 | # mark recipients not in common |
||
| 686 | for recipient in recipients: |
||
| 687 | if recipient.get("name") not in common_names: |
||
| 688 | recipient["valid"] = False |
||
| 689 | |||
| 690 | return recipients |
||
| 691 | |||
| 692 | def get_total_size(self, *files): |
||
| 693 | """Calculate the total size of the given files |
||
| 694 | """ |
||
| 695 | |||
| 696 | # Recursive unpack an eventual list of lists |
||
| 697 | def iterate(item): |
||
| 698 | if isinstance(item, (list, tuple)): |
||
| 699 | for i in item: |
||
| 700 | for ii in iterate(i): |
||
| 701 | yield ii |
||
| 702 | else: |
||
| 703 | yield item |
||
| 704 | |||
| 705 | # Calculate the total size of the given objects starting with an |
||
| 706 | # initial size of 0 |
||
| 707 | return functools.reduce(lambda x, y: x + y, |
||
| 708 | map(self.get_filesize, iterate(files)), 0) |
||
| 709 | |||
| 710 | def get_object_by_uid(self, uid): |
||
| 711 | """Get the object by UID |
||
| 712 | """ |
||
| 713 | logger.debug("get_object_by_uid::UID={}".format(uid)) |
||
| 714 | obj = api.get_object_by_uid(uid, None) |
||
| 715 | if obj is None: |
||
| 716 | logger.warn("!! No object found for UID #{} !!") |
||
| 717 | return obj |
||
| 718 | |||
| 719 | def get_filesize(self, f): |
||
| 720 | """Return the filesize of the PDF as a float |
||
| 721 | """ |
||
| 722 | try: |
||
| 723 | filesize = float(f.get_size()) |
||
| 724 | return float("%.2f" % (filesize / 1024)) |
||
| 725 | except (POSKeyError, TypeError, AttributeError): |
||
| 726 | return 0.0 |
||
| 727 | |||
| 728 | def get_report_filename(self, report): |
||
| 729 | """Generate the filename for the sample PDF |
||
| 730 | """ |
||
| 731 | sample = report.getAnalysisRequest() |
||
| 732 | return "{}.pdf".format(api.get_id(sample)) |
||
| 733 | |||
| 734 | def get_pdf(self, obj): |
||
| 735 | """Get the report PDF |
||
| 736 | """ |
||
| 737 | try: |
||
| 738 | return obj.getPdf() |
||
| 739 | except (POSKeyError, TypeError): |
||
| 740 | return None |
||
| 741 | |||
| 742 | View Code Duplication | def get_recipients(self, ar): |
|
| 743 | """Return the AR recipients in the same format like the AR Report |
||
| 744 | expects in the records field `Recipients` |
||
| 745 | """ |
||
| 746 | plone_utils = api.get_tool("plone_utils") |
||
| 747 | |||
| 748 | def is_email(email): |
||
| 749 | if not plone_utils.validateSingleEmailAddress(email): |
||
| 750 | return False |
||
| 751 | return True |
||
| 752 | |||
| 753 | def recipient_from_contact(contact): |
||
| 754 | if not contact: |
||
| 755 | return None |
||
| 756 | email = contact.getEmailAddress() |
||
| 757 | return { |
||
| 758 | "UID": api.get_uid(contact), |
||
| 759 | "Username": contact.getUsername(), |
||
| 760 | "Fullname": to_utf8(contact.Title()), |
||
| 761 | "EmailAddress": email, |
||
| 762 | } |
||
| 763 | |||
| 764 | def recipient_from_email(email): |
||
| 765 | if not is_email(email): |
||
| 766 | return None |
||
| 767 | return { |
||
| 768 | "UID": "", |
||
| 769 | "Username": "", |
||
| 770 | "Fullname": email, |
||
| 771 | "EmailAddress": email, |
||
| 772 | } |
||
| 773 | |||
| 774 | # Primary Contacts |
||
| 775 | to = filter(None, [recipient_from_contact(ar.getContact())]) |
||
| 776 | # CC Contacts |
||
| 777 | cc = filter(None, map(recipient_from_contact, ar.getCCContact())) |
||
| 778 | # CC Emails |
||
| 779 | cc_emails = ar.getCCEmails(as_list=True) |
||
| 780 | cc_emails = filter(None, map(recipient_from_email, cc_emails)) |
||
| 781 | |||
| 782 | return to + cc + cc_emails |
||
| 783 | |||
| 784 | def ajax_recalculate_size(self): |
||
| 785 | """Recalculate the total size of the selected attachments |
||
| 786 | """ |
||
| 787 | reports = self.reports |
||
| 788 | attachments = self.attachments |
||
| 789 | total_size = self.get_total_size(reports, attachments) |
||
| 790 | |||
| 791 | return { |
||
| 792 | "files": len(reports) + len(attachments), |
||
| 793 | "size": "%.2f" % total_size, |
||
| 794 | "limit": self.max_email_size, |
||
| 795 | "limit_exceeded": total_size > self.max_email_size, |
||
| 796 | } |
||
| 797 | |||
| 798 | def fail(self, message, status=500, **kw): |
||
| 799 | """Set a JSON error object and a status to the response |
||
| 800 | """ |
||
| 801 | self.request.response.setStatus(status) |
||
| 802 | result = {"success": False, "errors": message, "status": status} |
||
| 803 | result.update(kw) |
||
| 804 | return result |
||
| 805 |