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