bika.lims.browser.publish.emailview   F
last analyzed

Complexity

Total Complexity 112

Size/Duplication

Total Lines 836
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 112
eloc 516
dl 0
loc 836
rs 2
c 0
b 0
f 0

52 Methods

Rating   Name   Duplication   Size   Complexity  
A EmailView.form_action_send() 0 20 2
A EmailView.__call__() 0 29 5
A EmailView.publishTraverse() 0 8 1
A EmailView.handle_ajax_request() 0 25 3
A EmailView.form_action_cancel() 0 5 1
A EmailView.__init__() 0 8 1
A EmailView.validate_email_recipients() 0 13 3
A EmailView.write_sendlog() 0 17 2
A EmailView.portal() 0 5 1
A EmailView.validate_email_size() 0 14 2
B EmailView.get_responsibles_data() 0 41 7
A EmailView.render_email_template() 0 22 2
A EmailView.add_status_message() 0 4 1
A EmailView.exit_url() 0 9 2
B EmailView.get_recipients_data() 0 36 7
A EmailView.publish_samples() 0 13 3
A EmailView.recipients_data() 0 6 1
A EmailView.get_report_data() 0 24 3
A EmailView.setup() 0 5 1
A EmailView.get_total_size() 0 17 5
A EmailView.fail() 0 7 1
A EmailView.reports_data() 0 6 1
A EmailView.email_responsibles() 0 5 1
A EmailView.responsibles_data() 0 6 1
A EmailView.email_sender_name() 0 7 1
A EmailView.get_object_by_uid() 0 8 2
A EmailView.make_sendlog_record() 0 28 1
A EmailView.get_pdf() 0 7 2
A EmailView.lab_name() 0 6 1
A EmailView.laboratory() 0 5 1
A EmailView.client_name() 0 5 1
A EmailView.email_recipients_and_responsibles() 0 5 1
A EmailView.get_recipients() 0 41 4
A EmailView.email_recipients() 0 5 1
A EmailView.attachments() 0 7 1
B EmailView.validate_email_form() 0 32 7
A EmailView.reports() 0 13 2
A EmailView.get_all_sample_attachments() 0 17 4
A EmailView.email_sender_address() 0 6 1
A EmailView.email_subject() 0 10 2
A EmailView.lab_address() 0 6 1
A EmailView.publish() 0 19 2
A EmailView.ajax_recalculate_size() 0 12 1
A EmailView.always_cc_responsibles() 0 5 1
A EmailView.get_report_filename() 0 5 1
A EmailView.max_email_size() 0 12 3
A EmailView.email_attachments() 0 25 4
A EmailView.email_body() 0 18 2
A EmailView.total_size() 0 7 1
A EmailView.get_filesize() 0 8 2
A EmailView.send_email() 0 35 4
A EmailView.get_attachment_data() 0 22 1

How to fix   Complexity   

Complexity

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