Total Complexity | 104 |
Total Lines | 793 |
Duplicated Lines | 5.17 % |
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 laboratory(self): |
||
223 | """Laboratory object from the LIMS setup |
||
224 | """ |
||
225 | return api.get_setup().laboratory |
||
226 | |||
227 | @property |
||
228 | @view.memoize |
||
229 | def reports(self): |
||
230 | """Return the objects from the UIDs given in the request |
||
231 | """ |
||
232 | # Create a mapping of source ARs for copy |
||
233 | uids = self.request.form.get("uids", []) |
||
234 | # handle 'uids' GET parameter coming from a redirect |
||
235 | if isinstance(uids, six.string_types): |
||
236 | uids = uids.split(",") |
||
237 | uids = filter(api.is_uid, uids) |
||
238 | unique_uids = OrderedDict().fromkeys(uids).keys() |
||
239 | return map(self.get_object_by_uid, unique_uids) |
||
240 | |||
241 | @property |
||
242 | @view.memoize |
||
243 | def attachments(self): |
||
244 | """Return the objects from the UIDs given in the request |
||
245 | """ |
||
246 | uids = self.request.form.get("attachment_uids", []) |
||
247 | return map(self.get_object_by_uid, uids) |
||
248 | |||
249 | @property |
||
250 | def email_sender_address(self): |
||
251 | """Sender email is either the lab email or portal email "from" address |
||
252 | """ |
||
253 | lab_email = self.laboratory.getEmailAddress() |
||
254 | portal_email = api.get_registry_record("plone.email_from_address") |
||
255 | return lab_email or portal_email or "" |
||
256 | |||
257 | @property |
||
258 | def email_sender_name(self): |
||
259 | """Sender name is either the lab name or the portal email "from" name |
||
260 | """ |
||
261 | lab_from_name = self.laboratory.getName() |
||
262 | portal_from_name = api.get_registry_record("plone.email_from_name") |
||
263 | return lab_from_name or portal_from_name |
||
264 | |||
265 | @property |
||
266 | def email_recipients_and_responsibles(self): |
||
267 | """Returns a unified list of recipients and responsibles |
||
268 | """ |
||
269 | return list(set(self.email_recipients + self.email_responsibles)) |
||
270 | |||
271 | @property |
||
272 | def email_recipients(self): |
||
273 | """Email addresses of the selected recipients |
||
274 | """ |
||
275 | return map(safe_unicode, self.request.form.get("recipients", [])) |
||
276 | |||
277 | @property |
||
278 | def email_responsibles(self): |
||
279 | """Email addresses of the responsible persons |
||
280 | """ |
||
281 | return map(safe_unicode, self.request.form.get("responsibles", [])) |
||
282 | |||
283 | @property |
||
284 | def email_subject(self): |
||
285 | """Email subject line to be used in the template |
||
286 | """ |
||
287 | # request parameter has precedence |
||
288 | subject = self.request.get("subject", None) |
||
289 | if subject is not None: |
||
290 | return subject |
||
291 | subject = self.context.translate(_("Analysis Results for {}")) |
||
292 | return subject.format(self.client_name) |
||
293 | |||
294 | @property |
||
295 | def email_body(self): |
||
296 | """Email body text to be used in the template |
||
297 | """ |
||
298 | # request parameter has precedence |
||
299 | body = self.request.get("body", None) |
||
300 | if body is not None: |
||
301 | return body |
||
302 | setup = api.get_setup() |
||
303 | body = setup.getEmailBodySamplePublication() |
||
304 | template_context = { |
||
305 | "client_name": self.client_name, |
||
306 | "lab_name": self.lab_name, |
||
307 | "lab_address": self.lab_address, |
||
308 | } |
||
309 | rendered_body = self.render_email_template( |
||
310 | body, template_context=template_context) |
||
311 | return rendered_body |
||
312 | |||
313 | @property |
||
314 | def email_attachments(self): |
||
315 | attachments = [] |
||
316 | |||
317 | # Convert report PDFs -> email attachments |
||
318 | for report in self.reports: |
||
319 | pdf = self.get_pdf(report) |
||
320 | if pdf is None: |
||
321 | logger.error("Skipping empty PDF for report {}" |
||
322 | .format(report.getId())) |
||
323 | continue |
||
324 | filename = self.get_report_filename(report) |
||
325 | filedata = pdf.data |
||
326 | attachments.append( |
||
327 | mailapi.to_email_attachment(filedata, filename)) |
||
328 | |||
329 | # Convert additional attachments |
||
330 | for attachment in self.attachments: |
||
331 | af = attachment.getAttachmentFile() |
||
332 | filedata = af.data |
||
333 | filename = af.filename |
||
334 | attachments.append( |
||
335 | mailapi.to_email_attachment(filedata, filename)) |
||
336 | |||
337 | return attachments |
||
338 | |||
339 | @property |
||
340 | def reports_data(self): |
||
341 | """Returns a list of report data dictionaries |
||
342 | """ |
||
343 | reports = self.reports |
||
344 | return map(self.get_report_data, reports) |
||
345 | |||
346 | @property |
||
347 | def recipients_data(self): |
||
348 | """Returns a list of recipients data dictionaries |
||
349 | """ |
||
350 | reports = self.reports |
||
351 | return self.get_recipients_data(reports) |
||
352 | |||
353 | @property |
||
354 | def responsibles_data(self): |
||
355 | """Returns a list of responsibles data dictionaries |
||
356 | """ |
||
357 | reports = self.reports |
||
358 | return self.get_responsibles_data(reports) |
||
359 | |||
360 | @property |
||
361 | def client_name(self): |
||
362 | """Returns the client name |
||
363 | """ |
||
364 | return safe_unicode(self.context.Title()) |
||
365 | |||
366 | @property |
||
367 | def lab_address(self): |
||
368 | """Returns the laboratory print address |
||
369 | """ |
||
370 | return "<br/>".join(self.laboratory.getPrintAddress()) |
||
371 | |||
372 | @property |
||
373 | def lab_name(self): |
||
374 | """Returns the laboratory name |
||
375 | """ |
||
376 | return self.laboratory.getName() |
||
377 | |||
378 | @property |
||
379 | def exit_url(self): |
||
380 | """Exit URL for redirect |
||
381 | """ |
||
382 | endpoint = "reports_listing" |
||
383 | if IAnalysisRequest.providedBy(self.context): |
||
384 | endpoint = "published_results" |
||
385 | return "{}/{}".format( |
||
386 | api.get_url(self.context), endpoint) |
||
387 | |||
388 | @property |
||
389 | def total_size(self): |
||
390 | """Total size of all report PDFs + additional attachments |
||
391 | """ |
||
392 | reports = self.reports |
||
393 | attachments = self.attachments |
||
394 | return self.get_total_size(reports, attachments) |
||
395 | |||
396 | @property |
||
397 | def max_email_size(self): |
||
398 | """Return the max. allowed email size in KB |
||
399 | """ |
||
400 | # check first if a registry record exists |
||
401 | max_email_size = api.get_registry_record( |
||
402 | "senaite.core.max_email_size") |
||
403 | if max_email_size is None: |
||
404 | max_size = DEFAULT_MAX_EMAIL_SIZE |
||
405 | if max_size < 0: |
||
|
|||
406 | max_email_size = 0 |
||
407 | return max_size * 1024 |
||
408 | |||
409 | def make_sendlog_record(self, **kw): |
||
410 | """Create a new sendlog record |
||
411 | """ |
||
412 | user = get_user() |
||
413 | actor = get_user_id() |
||
414 | userprops = api.get_user_properties(user) |
||
415 | actor_fullname = userprops.get("fullname", actor) |
||
416 | email_send_date = DateTime() |
||
417 | email_recipients = self.email_recipients |
||
418 | email_responsibles = self.email_responsibles |
||
419 | email_subject = self.email_subject |
||
420 | email_body = self.render_email_template(self.email_body) |
||
421 | email_attachments = map(api.get_uid, self.attachments) |
||
422 | |||
423 | record = { |
||
424 | "actor": actor, |
||
425 | "actor_fullname": actor_fullname, |
||
426 | "email_send_date": email_send_date, |
||
427 | "email_recipients": email_recipients, |
||
428 | "email_responsibles": email_responsibles, |
||
429 | "email_subject": email_subject, |
||
430 | "email_body": email_body, |
||
431 | "email_attachments": email_attachments, |
||
432 | |||
433 | } |
||
434 | # keywords take precedence |
||
435 | record.update(kw) |
||
436 | return record |
||
437 | |||
438 | def write_sendlog(self): |
||
439 | """Write email sendlog |
||
440 | """ |
||
441 | timestamp = DateTime() |
||
442 | |||
443 | for report in self.reports: |
||
444 | # get the current sendlog records |
||
445 | records = report.getSendLog() |
||
446 | # create a new record with the current data |
||
447 | new_record = self.make_sendlog_record(email_send_date=timestamp) |
||
448 | # set the new record to the existing records |
||
449 | records.append(new_record) |
||
450 | report.setSendLog(records) |
||
451 | # reindex object to make changes visible in the snapshot |
||
452 | report.reindexObject() |
||
453 | # manually take a new snapshot |
||
454 | take_snapshot(report) |
||
455 | |||
456 | def publish_samples(self): |
||
457 | """Publish all samples of the reports |
||
458 | """ |
||
459 | samples = set() |
||
460 | |||
461 | # collect primary + contained samples of the reports |
||
462 | for report in self.reports: |
||
463 | samples.add(report.getAnalysisRequest()) |
||
464 | samples.update(report.getContainedAnalysisRequests()) |
||
465 | |||
466 | # publish all samples + their partitions |
||
467 | for sample in samples: |
||
468 | self.publish(sample) |
||
469 | |||
470 | def publish(self, sample): |
||
471 | """Set status to prepublished/published/republished |
||
472 | """ |
||
473 | wf = api.get_tool("portal_workflow") |
||
474 | status = wf.getInfoFor(sample, "review_state") |
||
475 | transitions = {"verified": "publish", |
||
476 | "published": "republish"} |
||
477 | transition = transitions.get(status, "prepublish") |
||
478 | logger.info("Transitioning sample {}: {} -> {}".format( |
||
479 | api.get_id(sample), status, transition)) |
||
480 | try: |
||
481 | # Manually update the view on the database to avoid conflict errors |
||
482 | sample.getClient()._p_jar.sync() |
||
483 | # Perform WF transition |
||
484 | wf.doActionFor(sample, transition) |
||
485 | # Commit the changes |
||
486 | transaction.commit() |
||
487 | except WorkflowException as e: |
||
488 | logger.error(e) |
||
489 | |||
490 | def render_email_template(self, template, template_context=None): |
||
491 | """Return the rendered email template |
||
492 | |||
493 | This method interpolates the $recipients variable with the selected |
||
494 | recipients from the email form. |
||
495 | |||
496 | :params template: Email body text |
||
497 | :returns: Rendered email template |
||
498 | """ |
||
499 | |||
500 | # allow to add translation for initial template |
||
501 | template = self.context.translate(_(template)) |
||
502 | recipients = self.email_recipients_and_responsibles |
||
503 | if template_context is None: |
||
504 | template_context = { |
||
505 | "recipients": "<br/>".join(recipients), |
||
506 | } |
||
507 | |||
508 | email_template = Template(safe_unicode(template)).safe_substitute( |
||
509 | **template_context) |
||
510 | |||
511 | return email_template |
||
512 | |||
513 | def send_email(self, recipients, subject, body, attachments=None): |
||
514 | """Prepare and send email to the recipients |
||
515 | |||
516 | :param recipients: a list of email or name,email strings |
||
517 | :param subject: the email subject |
||
518 | :param body: the email body |
||
519 | :param attachments: list of email attachments |
||
520 | :returns: True if all emails were sent, else False |
||
521 | """ |
||
522 | email_body = self.render_email_template(body) |
||
523 | |||
524 | success = [] |
||
525 | # Send one email per recipient |
||
526 | for recipient in recipients: |
||
527 | # N.B. we use just the email here to prevent this Postfix Error: |
||
528 | # Recipient address rejected: User unknown in local recipient table |
||
529 | pair = mailapi.parse_email_address(recipient) |
||
530 | to_address = pair[1] |
||
531 | mime_msg = mailapi.compose_email(self.email_sender_address, |
||
532 | to_address, |
||
533 | subject, |
||
534 | email_body, |
||
535 | attachments=attachments, |
||
536 | html=True) |
||
537 | sent = mailapi.send_email(mime_msg) |
||
538 | if not sent: |
||
539 | msg = _("Could not send email to {0} ({1})").format(pair[0], |
||
540 | pair[1]) |
||
541 | self.add_status_message(msg, "warning") |
||
542 | logger.error(msg) |
||
543 | success.append(sent) |
||
544 | |||
545 | if not all(success): |
||
546 | return False |
||
547 | return True |
||
548 | |||
549 | def add_status_message(self, message, level="info"): |
||
550 | """Set a portal status message |
||
551 | """ |
||
552 | return self.context.plone_utils.addPortalMessage(message, level) |
||
553 | |||
554 | def get_report_data(self, report): |
||
555 | """Report data to be used in the template |
||
556 | """ |
||
557 | sample = report.getAnalysisRequest() |
||
558 | analyses = sample.getAnalyses(full_objects=True) |
||
559 | # merge together sample + analyses attachments |
||
560 | attachments = itertools.chain( |
||
561 | sample.getAttachment(), |
||
562 | *map(lambda an: an.getAttachment(), analyses)) |
||
563 | attachments_data = map(self.get_attachment_data, attachments) |
||
564 | pdf = self.get_pdf(report) |
||
565 | filesize = "{} Kb".format(self.get_filesize(pdf)) |
||
566 | filename = self.get_report_filename(report) |
||
567 | |||
568 | return { |
||
569 | "sample": sample, |
||
570 | "attachments": attachments_data, |
||
571 | "pdf": pdf, |
||
572 | "obj": report, |
||
573 | "uid": api.get_uid(report), |
||
574 | "filesize": filesize, |
||
575 | "filename": filename, |
||
576 | } |
||
577 | |||
578 | def get_attachment_data(self, attachment): |
||
579 | """Attachments data to be used in the template |
||
580 | """ |
||
581 | f = attachment.getAttachmentFile() |
||
582 | attachment_type = attachment.getAttachmentType() |
||
583 | attachment_keys = attachment.getAttachmentKeys() |
||
584 | filename = f.filename |
||
585 | filesize = self.get_filesize(f) |
||
586 | mimetype = f.getContentType() |
||
587 | report_option = attachment.getReportOption() |
||
588 | |||
589 | return { |
||
590 | "obj": attachment, |
||
591 | "attachment_type": attachment_type, |
||
592 | "attachment_keys": attachment_keys, |
||
593 | "file": f, |
||
594 | "uid": api.get_uid(attachment), |
||
595 | "filesize": filesize, |
||
596 | "filename": filename, |
||
597 | "mimetype": mimetype, |
||
598 | "report_option": report_option, |
||
599 | } |
||
600 | |||
601 | def get_recipients_data(self, reports): |
||
602 | """Recipients data to be used in the template |
||
603 | """ |
||
604 | if not reports: |
||
605 | return [] |
||
606 | |||
607 | recipients = [] |
||
608 | recipient_names = [] |
||
609 | |||
610 | for num, report in enumerate(reports): |
||
611 | sample = report.getAnalysisRequest() |
||
612 | # recipient names of this report |
||
613 | report_recipient_names = [] |
||
614 | for recipient in self.get_recipients(sample): |
||
615 | name = recipient.get("Fullname") |
||
616 | email = recipient.get("EmailAddress") |
||
617 | address = mailapi.to_email_address(email, name=name) |
||
618 | record = { |
||
619 | "name": name, |
||
620 | "email": email, |
||
621 | "address": address, |
||
622 | "valid": True, |
||
623 | } |
||
624 | if record not in recipients: |
||
625 | recipients.append(record) |
||
626 | # remember the name of the recipient for this report |
||
627 | report_recipient_names.append(name) |
||
628 | recipient_names.append(report_recipient_names) |
||
629 | |||
630 | # recipient names, which all of the reports have in common |
||
631 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
632 | # mark recipients not in common |
||
633 | for recipient in recipients: |
||
634 | if recipient.get("name") not in common_names: |
||
635 | recipient["valid"] = False |
||
636 | return recipients |
||
637 | |||
638 | def get_responsibles_data(self, reports): |
||
639 | """Responsibles data to be used in the template |
||
640 | """ |
||
641 | if not reports: |
||
642 | return [] |
||
643 | |||
644 | recipients = [] |
||
645 | recipient_names = [] |
||
646 | |||
647 | for num, report in enumerate(reports): |
||
648 | # get the linked AR of this ARReport |
||
649 | ar = report.getAnalysisRequest() |
||
650 | |||
651 | # recipient names of this report |
||
652 | report_recipient_names = [] |
||
653 | responsibles = ar.getResponsible() |
||
654 | for manager_id in responsibles.get("ids", []): |
||
655 | responsible = responsibles["dict"][manager_id] |
||
656 | name = responsible.get("name") |
||
657 | email = responsible.get("email") |
||
658 | address = mailapi.to_email_address(email, name=name) |
||
659 | record = { |
||
660 | "name": name, |
||
661 | "email": email, |
||
662 | "address": address, |
||
663 | "valid": True, |
||
664 | } |
||
665 | if record not in recipients: |
||
666 | recipients.append(record) |
||
667 | # remember the name of the recipient for this report |
||
668 | report_recipient_names.append(name) |
||
669 | recipient_names.append(report_recipient_names) |
||
670 | |||
671 | # recipient names, which all of the reports have in common |
||
672 | common_names = set(recipient_names[0]).intersection(*recipient_names) |
||
673 | # mark recipients not in common |
||
674 | for recipient in recipients: |
||
675 | if recipient.get("name") not in common_names: |
||
676 | recipient["valid"] = False |
||
677 | |||
678 | return recipients |
||
679 | |||
680 | def get_total_size(self, *files): |
||
681 | """Calculate the total size of the given files |
||
682 | """ |
||
683 | |||
684 | # Recursive unpack an eventual list of lists |
||
685 | def iterate(item): |
||
686 | if isinstance(item, (list, tuple)): |
||
687 | for i in item: |
||
688 | for ii in iterate(i): |
||
689 | yield ii |
||
690 | else: |
||
691 | yield item |
||
692 | |||
693 | # Calculate the total size of the given objects starting with an |
||
694 | # initial size of 0 |
||
695 | return functools.reduce(lambda x, y: x + y, |
||
696 | map(self.get_filesize, iterate(files)), 0) |
||
697 | |||
698 | def get_object_by_uid(self, uid): |
||
699 | """Get the object by UID |
||
700 | """ |
||
701 | logger.debug("get_object_by_uid::UID={}".format(uid)) |
||
702 | obj = api.get_object_by_uid(uid, None) |
||
703 | if obj is None: |
||
704 | logger.warn("!! No object found for UID #{} !!") |
||
705 | return obj |
||
706 | |||
707 | def get_filesize(self, f): |
||
708 | """Return the filesize of the PDF as a float |
||
709 | """ |
||
710 | try: |
||
711 | filesize = float(f.get_size()) |
||
712 | return float("%.2f" % (filesize / 1024)) |
||
713 | except (POSKeyError, TypeError, AttributeError): |
||
714 | return 0.0 |
||
715 | |||
716 | def get_report_filename(self, report): |
||
717 | """Generate the filename for the sample PDF |
||
718 | """ |
||
719 | sample = report.getAnalysisRequest() |
||
720 | return "{}.pdf".format(api.get_id(sample)) |
||
721 | |||
722 | def get_pdf(self, obj): |
||
723 | """Get the report PDF |
||
724 | """ |
||
725 | try: |
||
726 | return obj.getPdf() |
||
727 | except (POSKeyError, TypeError): |
||
728 | return None |
||
729 | |||
730 | View Code Duplication | def get_recipients(self, ar): |
|
731 | """Return the AR recipients in the same format like the AR Report |
||
732 | expects in the records field `Recipients` |
||
733 | """ |
||
734 | plone_utils = api.get_tool("plone_utils") |
||
735 | |||
736 | def is_email(email): |
||
737 | if not plone_utils.validateSingleEmailAddress(email): |
||
738 | return False |
||
739 | return True |
||
740 | |||
741 | def recipient_from_contact(contact): |
||
742 | if not contact: |
||
743 | return None |
||
744 | email = contact.getEmailAddress() |
||
745 | return { |
||
746 | "UID": api.get_uid(contact), |
||
747 | "Username": contact.getUsername(), |
||
748 | "Fullname": to_utf8(contact.Title()), |
||
749 | "EmailAddress": email, |
||
750 | } |
||
751 | |||
752 | def recipient_from_email(email): |
||
753 | if not is_email(email): |
||
754 | return None |
||
755 | return { |
||
756 | "UID": "", |
||
757 | "Username": "", |
||
758 | "Fullname": email, |
||
759 | "EmailAddress": email, |
||
760 | } |
||
761 | |||
762 | # Primary Contacts |
||
763 | to = filter(None, [recipient_from_contact(ar.getContact())]) |
||
764 | # CC Contacts |
||
765 | cc = filter(None, map(recipient_from_contact, ar.getCCContact())) |
||
766 | # CC Emails |
||
767 | cc_emails = ar.getCCEmails(as_list=True) |
||
768 | cc_emails = filter(None, map(recipient_from_email, cc_emails)) |
||
769 | |||
770 | return to + cc + cc_emails |
||
771 | |||
772 | def ajax_recalculate_size(self): |
||
773 | """Recalculate the total size of the selected attachments |
||
774 | """ |
||
775 | reports = self.reports |
||
776 | attachments = self.attachments |
||
777 | total_size = self.get_total_size(reports, attachments) |
||
778 | |||
779 | return { |
||
780 | "files": len(reports) + len(attachments), |
||
781 | "size": "%.2f" % total_size, |
||
782 | "limit": self.max_email_size, |
||
783 | "limit_exceeded": total_size > self.max_email_size, |
||
784 | } |
||
785 | |||
786 | def fail(self, message, status=500, **kw): |
||
787 | """Set a JSON error object and a status to the response |
||
788 | """ |
||
789 | self.request.response.setStatus(status) |
||
790 | result = {"success": False, "errors": message, "status": status} |
||
791 | result.update(kw) |
||
792 | return result |
||
793 |