Passed
Push — 2.x ( 107842...b18e75 )
by Ramon
07:21
created

bika.lims.browser.publish.emailview   F

Complexity

Total Complexity 110

Size/Duplication

Total Lines 823
Duplicated Lines 4.98 %

Importance

Changes 0
Metric Value
wmc 110
eloc 508
dl 41
loc 823
rs 2
c 0
b 0
f 0

51 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.recipients_data() 0 6 1
A EmailView.setup() 0 5 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.laboratory() 0 5 1
A EmailView.client_name() 0 5 1
A EmailView.email_recipients_and_responsibles() 0 5 1
A EmailView.email_recipients() 0 5 1
A EmailView.attachments() 0 7 1
A EmailView.reports() 0 13 2
A EmailView.email_sender_address() 0 7 1
A EmailView.email_subject() 0 10 2
A EmailView.always_cc_responsibles() 0 5 1
A EmailView.email_attachments() 0 25 4
A EmailView.email_body() 0 18 2
A EmailView.write_sendlog() 0 17 2
A EmailView.render_email_template() 0 22 2
A EmailView.add_status_message() 0 4 1
A EmailView.exit_url() 0 9 2
A EmailView.publish_samples() 0 13 3
A EmailView.make_sendlog_record() 0 28 1
A EmailView.lab_name() 0 6 1
A EmailView.lab_address() 0 6 1
A EmailView.publish() 0 19 2
A EmailView.max_email_size() 0 12 3
A EmailView.total_size() 0 7 1
A EmailView.send_email() 0 35 4
B EmailView.get_responsibles_data() 0 41 7
B EmailView.get_recipients_data() 0 36 7
B EmailView.get_report_data() 0 38 6
A EmailView.get_total_size() 0 17 5
A EmailView.fail() 0 7 1
A EmailView.get_object_by_uid() 0 8 2
A EmailView.get_pdf() 0 7 2
A EmailView.get_recipients() 41 41 4
A EmailView.ajax_recalculate_size() 0 12 1
A EmailView.get_report_filename() 0 5 1
A EmailView.get_filesize() 0 8 2
A EmailView.get_attachment_data() 0 21 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.browser.publish.emailview often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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