Passed
Push — 2.x ( ab0781...5609a0 )
by Jordi
06:07
created

EmailView.email_recipients()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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