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

Complexity

Total Complexity 111

Size/Duplication

Total Lines 827
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 111
eloc 510
dl 0
loc 827
rs 2
c 0
b 0
f 0

52 Methods

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