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