• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

collective / collective.documentgenerator / 9284190221

29 May 2024 10:06AM UTC coverage: 91.912% (+0.5%) from 91.42%
9284190221

Pull #60

github

duchenean
fix broken tests en P4
Pull Request #60: Plone 6 compatibility

70 of 91 new or added lines in 15 files covered. (76.92%)

1 existing line in 1 file now uncovered.

2182 of 2374 relevant lines covered (91.91%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.66
/src/collective/documentgenerator/browser/generation_view.py
1
# -*- coding: utf-8 -*-
2

3
from .. import _
1✔
4
from AccessControl import Unauthorized
1✔
5
from appy.pod.renderer import CsvOptions
1✔
6
from appy.pod.renderer import Renderer
1✔
7
from appy.pod.styles_manager import TableProperties
1✔
8
from collective.documentgenerator import config
1✔
9
from collective.documentgenerator import utils
1✔
10
from collective.documentgenerator.content.pod_template import IPODTemplate
1✔
11
from collective.documentgenerator.interfaces import CyclicMergeTemplatesException
1✔
12
from collective.documentgenerator.interfaces import IDocumentFactory
1✔
13
from collective.documentgenerator.interfaces import PODTemplateNotFoundError
1✔
14
from collective.documentgenerator.utils import remove_tmp_file
1✔
15
from collective.documentgenerator.utils import temporary_file_name
1✔
16
from imio.helpers import HAS_PLONE_5_AND_MORE
1✔
17
from plone import api
1✔
18
from plone.app.uuid.utils import uuidToObject
1✔
19
from plone.i18n.normalizer.interfaces import IFileNameNormalizer
1✔
20
from Products.CMFPlone.utils import base_hasattr
1✔
21
from Products.CMFPlone.utils import safe_unicode
1✔
22
from Products.Five import BrowserView
1✔
23
from six import StringIO
1✔
24
from zope.annotation.interfaces import IAnnotations
1✔
25
from zope.component import getMultiAdapter
1✔
26
from zope.component import queryAdapter
1✔
27
from zope.component import queryUtility
1✔
28

29
import io
1✔
30
import mimetypes
1✔
31
import os
1✔
32
import pkg_resources
1✔
33
import six
1✔
34
import unicodedata
1✔
35

36

37
HAS_FINGERPOINTING = None
1✔
38

39
try:
1✔
40
    pkg_resources.get_distribution('collective.fingerpointing')
1✔
41
except pkg_resources.DistributionNotFound:
×
42
    HAS_FINGERPOINTING = False
×
43
else:
44
    HAS_FINGERPOINTING = True
1✔
45

46

47
class DocumentGenerationView(BrowserView):
1✔
48
    """
49
    Document generation view.
50
    """
51

52
    def __init__(self, context, request):
1✔
53
        self.context = context
1✔
54
        self.request = request
1✔
55
        self.pod_template = None
1✔
56
        self.output_format = None
1✔
57

58
    def __call__(self, template_uid='', output_format='', **kwargs):
1✔
59
        self.pod_template, self.output_format = self._get_base_args(template_uid, output_format)
1✔
60
        return self.generate_and_download_doc(self.pod_template, self.output_format, **kwargs)
1✔
61

62
    def _get_base_args(self, template_uid, output_format):
1✔
63
        template_uid = template_uid or self.get_pod_template_uid()
1✔
64
        pod_template = self.get_pod_template(template_uid)
1✔
65
        output_format = output_format or self.get_generation_format()
1✔
66
        if not output_format:
1✔
67
            raise Exception("No 'output_format' found to generate this document")
1✔
68

69
        return pod_template, output_format
1✔
70

71
    def generate_and_download_doc(self, pod_template, output_format, **kwargs):
1✔
72
        """
73
        Generate a document of format 'output_format' from the template
74
        'pod_template' and return it as a downloadable file.
75
        """
76
        if HAS_FINGERPOINTING:
1✔
77
            from collective.fingerpointing.config import AUDIT_MESSAGE
1✔
78
            from collective.fingerpointing.logger import log_info
1✔
79
            from collective.fingerpointing.utils import get_request_information
1✔
80

81
            # add logging message to fingerpointing log
82
            user, ip = get_request_information()
1✔
83
            action = 'generate_document'
1✔
84
            extras = 'context={0} pod_template={1} output_format={2}'.format(
1✔
85
                '/'.join(self.context.getPhysicalPath()),
86
                '/'.join(pod_template.getPhysicalPath()),
87
                output_format)
88
            allowed_parameters = filter(
1✔
89
                None,
90
                os.getenv("DOCUMENTGENERATOR_LOG_PARAMETERS", "").split(",")
91
            )
92
            if allowed_parameters:
1✔
93
                for key, value in self.request.form.items():
1✔
94
                    if key in allowed_parameters:
×
95
                        extras = "{0} {1}={2}".format(extras, key, value)
×
96
            log_info(AUDIT_MESSAGE.format(user, ip, action, extras))
1✔
97

98
        doc, doc_name, gen_context = self._generate_doc(pod_template, output_format, **kwargs)
1✔
99
        self._set_header_response(doc_name)
1✔
100
        return doc
1✔
101

102
    def _generate_doc(self, pod_template, output_format, **kwargs):
1✔
103
        """
104
        Generate a document of format 'output_format' from the template
105
        'pod_template'.
106
        """
107
        if not pod_template.can_be_generated(self.context):
1✔
108
            raise Unauthorized('You are not allowed to generate this document.')
1✔
109

110
        if output_format not in pod_template.get_available_formats():
1✔
111
            raise Exception(
1✔
112
                "Asked output format '{0}' "
113
                "is not available for template '{1}'!".format(
114
                    output_format,
115
                    pod_template.getId()
116
                )
117
            )
118

119
        # subtemplates should not refer to each other in a cyclic way.
120
        self._check_cyclic_merges(pod_template)
1✔
121

122
        # Recursive generation of the document and all its subtemplates.
123
        document_path, gen_context = self._recursive_generate_doc(pod_template, output_format, **kwargs)
1✔
124

125
        rendered_document = open(document_path, 'rb')
1✔
126
        rendered = rendered_document.read()
1✔
127
        rendered_document.close()
1✔
128
        remove_tmp_file(document_path)
1✔
129
        filename = self._get_filename()
1✔
130
        return rendered, filename, gen_context
1✔
131

132
    def _get_filename(self):
1✔
133
        """ """
134
        # we limit filename to 120 characters
135
        first_part = u'{0} {1}'.format(self.pod_template.title, safe_unicode(self.context.Title()))
1✔
136
        # replace unicode special characters with ascii equivalent value
137
        first_part = unicodedata.normalize('NFKD', first_part).encode('ascii', 'ignore')
1✔
138
        util = queryUtility(IFileNameNormalizer)
1✔
139
        # remove '-' from first_part because it is handled by cropName that manages max_length
140
        # and it behaves weirdly if it encounters '-'
141
        # moreover avoid more than one blank space at a time
142
        first_part = u' '.join(util.normalize(first_part).replace(u'-', u' ').split()).strip()
1✔
143
        filename = '{0}.{1}'.format(util.normalize(first_part, max_length=120), self.output_format)
1✔
144
        return filename
1✔
145

146
    def _recursive_generate_doc(self, pod_template, output_format, **kwargs):
1✔
147
        """
148
        Generate a document recursively by starting to generate all its
149
        subtemplates before merging them in the final document.
150
        Return the file path of the generated document.
151
        """
152
        sub_templates = pod_template.get_templates_to_merge()
1✔
153
        sub_documents = {}
1✔
154
        for context_name, (sub_pod, do_rendering) in iter(sub_templates.items()):
1✔
155
            # Force the subtemplate output_format to 'odt' because appy.pod
156
            # can only merge documents in this format.
157
            if do_rendering:
1✔
158
                sub_documents[context_name] = self._recursive_generate_doc(
1✔
159
                    pod_template=sub_pod,
160
                    output_format='odt'
161
                )[0]
162
            else:
163
                sub_documents[context_name] = sub_pod
1✔
164

165
        document_path, gen_context = self._render_document(pod_template, output_format, sub_documents, **kwargs)
1✔
166

167
        return document_path, gen_context
1✔
168

169
    def get_pod_template(self, template_uid):
1✔
170
        """
171
        Return the default PODTemplate that will be used when calling
172
        this view.
173
        """
174
        catalog = api.portal.get_tool('portal_catalog')
1✔
175

176
        template_brains = catalog.unrestrictedSearchResults(
1✔
177
            object_provides=IPODTemplate.__identifier__,
178
            UID=template_uid
179
        )
180
        if not template_brains:
1✔
181
            raise PODTemplateNotFoundError(
1✔
182
                "Couldn't find POD template with UID '{0}'".format(template_uid)
183
            )
184

185
        template_path = template_brains[0].getPath()
1✔
186
        pod_template = self.context.unrestrictedTraverse(template_path)
1✔
187
        return pod_template
1✔
188

189
    def get_pod_template_uid(self):
1✔
190
        template_uid = self.request.get('template_uid', '')
1✔
191
        return template_uid
1✔
192

193
    def get_generation_format(self):
1✔
194
        """
195
        Return the default document output format that will be used
196
        when calling this view.
197
        """
198
        output_format = self.request.get('output_format')
1✔
199
        return output_format
1✔
200

201
    def _render_document(self, pod_template, output_format, sub_documents, raiseOnError=False, **kwargs):
1✔
202
        """
203
        Render a single document of type 'output_format' using the odt file
204
        'document_template' as the generation template.
205
        Subdocuments is a dictionnary of previously generated subtemplate
206
        that will be merged into the current generated document.
207
        """
208
        document_template = pod_template.get_file()
1✔
209
        temp_filename = temporary_file_name('.{extension}'.format(extension=output_format))
1✔
210

211
        # Prepare rendering context
212
        helper_view = self.get_generation_context_helper()
1✔
213
        generation_context = self._get_generation_context(helper_view, pod_template=pod_template)
1✔
214

215
        # enrich the generation context with previously generated documents
216
        utils.update_dict_with_validation(generation_context, sub_documents,
1✔
217
                                          _("Error when merging merge_templates in generation context"))
218

219
        # enable optimalColumnWidths if enabled in the config and/or on ConfigurablePodTemplate
220
        stylesMapping = {}
1✔
221
        optimalColumnWidths = "OCW_.*"
1✔
222
        distributeColumns = "DC_.*"
1✔
223

224
        column_modifier = pod_template.get_column_modifier()
1✔
225
        if column_modifier == -1:
1✔
226
            column_modifier = config.get_column_modifier()
1✔
227

228
        if column_modifier == 'disabled':
1✔
229
            optimalColumnWidths = False
1✔
230
            distributeColumns = False
1✔
231
        else:
232
            stylesMapping = {
1✔
233
                'table': TableProperties(columnModifier=column_modifier != 'nothing' and column_modifier or None)}
234

235
        # if raiseOnError is not enabled, enabled it in the config excepted if user is a Manager
236
        # and currently generated document use odt format
237
        if not raiseOnError:
1✔
238
            if config.get_raiseOnError_for_non_managers():
1✔
239
                raiseOnError = True
1✔
240
                if 'Manager' in api.user.get_roles() and output_format == 'odt':
1✔
241
                    raiseOnError = False
1✔
242

243
        # stylesMapping.update({'para[class=None, parent != cell]': 'texte_delibe',
244
        #                       'para[class=xSmallText, parent != cell]': 'bodyXSmall',
245
        #                       'para[class=smallText, parent != cell]': 'bodySmall',
246
        #                       'para[class=largeText, parent != cell]': 'bodyLarge',
247
        #                       'para[class=xLargeText, parent != cell]': 'bodyXLarge',
248
        #                       'para[class=indentation, parent != cell]': 'bodyIndentation',
249
        #                       'para[class=None, parent = cell]': 'cell_delibe',
250
        #                       'table': TableProperties(cellContentStyle='cell_delibe'),
251
        #                       'para[class=xSmallText, parent = cell]': 'cellXSmall',
252
        #                       'para[class=smallText, parent = cell]': 'cellSmall',
253
        #                       'para[class=largeText, parent = cell]': 'cellLarge',
254
        #                       'para[class=xLargeText, parent = cell]': 'cellXLarge',
255
        #                       'para[class=indentation, parent = cell]': 'cellIndentation',
256
        #                       })
257
        # stylesMapping.update({'para[class=None,parent!=cell]': 'texte_delibe',
258
        #                       'para[class=xSmallText,parent!=cell]': 'bodyXSmall',
259
        #                       'para[class=smallText,parent!=cell]': 'bodySmall',
260
        #                       'para[class=largeText,parent!=cell]': 'bodyLarge',
261
        #                       'para[class=xLargeText,parent!=cell]': 'bodyXLarge',
262
        #                       'para[class=indentation,parent!=cell]': 'bodyIndentation',
263
        #                       'para[class=xSmallText,parent=cell]': 'cellXSmall',
264
        #                       'para[class=smallText,parent=cell]': 'cellSmall',
265
        #                       'para[class=largeText,parent=cell]': 'cellLarge',
266
        #                       'para[class=xLargeText,parent=cell]': 'cellXLarge',
267
        #                       'para[class=indentation,parent=cell]': 'cellIndentation',
268
        #                       })
269

270
        csvOptions = None
1✔
271

272
        if output_format == "csv":
1✔
273
            csvOptions = CsvOptions(fieldSeparator=pod_template.csv_field_delimiter,
×
274
                                    textDelimiter=pod_template.csv_string_delimiter)
275
        if six.PY2:
1✔
NEW
276
            renderer = Renderer(
×
277
                StringIO(document_template.data),
278
                generation_context,
279
                temp_filename,
280
                pythonWithUnoPath=config.get_uno_path(),
281
                ooServer=config.get_oo_server(),
282
                ooPort=config.get_oo_port_list(),
283
                raiseOnError=raiseOnError,
284
                imageResolver=api.portal.get(),
285
                forceOoCall=True,
286
                html=True,
287
                optimalColumnWidths=optimalColumnWidths,
288
                distributeColumns=distributeColumns,
289
                stylesMapping=stylesMapping,
290
                stream=config.get_use_stream(),
291
                csvOptions=csvOptions,
292
                # deleteTempFolder=False,
293
                **kwargs
294
            )
295
        else:
296
            renderer = Renderer(
1✔
297
                io.BytesIO(document_template.data),
298
                generation_context,
299
                temp_filename,
300
                pythonWithUnoPath=config.get_uno_path(),
301
                ooServer=config.get_oo_server(),
302
                ooPort=config.get_oo_port_list(),
303
                raiseOnError=raiseOnError,
304
                findImage=api.portal.get(),
305
                forceOoCall=True,
306
                html=True,
307
                optimalColumnWidths=optimalColumnWidths,
308
                distributeColumns=distributeColumns,
309
                stylesMapping=stylesMapping,
310
                stream=config.get_use_stream(),
311
                csvOptions=csvOptions,
312
                # deleteTempFolder=False,
313
                **kwargs
314
            )
315

316
        # it is only now that we can initialize helper view's appy pod renderer
317
        all_helper_views = self.get_views_for_appy_renderer(generation_context, helper_view)
1✔
318
        for view in all_helper_views:
1✔
319
            view._set_appy_renderer(renderer)
1✔
320

321
        renderer.run()
1✔
322

323
        # return also generation_context to test ist content in tests
324
        return temp_filename, generation_context
1✔
325

326
    def _get_context_variables(self, pod_template):
1✔
327
        if base_hasattr(pod_template, 'get_context_variables'):
1✔
328
            return pod_template.get_context_variables()
1✔
329
        return {}
1✔
330

331
    def _get_generation_context(self, helper_view, pod_template):
1✔
332
        """
333
        Return the generation context for the current document.
334
        """
335
        generation_context = self.get_base_generation_context(helper_view, pod_template)
1✔
336
        utils.update_dict_with_validation(generation_context,
1✔
337
                                          {'context': getattr(helper_view, 'context', None),
338
                                           'portal': api.portal.get(),
339
                                           'view': helper_view},
340
                                          _("Error when merging helper_view in generation context"))
341
        utils.update_dict_with_validation(generation_context, self._get_context_variables(pod_template),
1✔
342
                                          _("Error when merging context_variables in generation context"))
343
        return generation_context
1✔
344

345
    def get_base_generation_context(self, helper_view, pod_template):
1✔
346
        """
347
        Override this method to provide your own generation context.
348
        """
349
        return {}
1✔
350

351
    def get_generation_context_helper(self):
1✔
352
        """
353
        Return the default helper view used for document generation.
354
        """
355
        helper_view = getMultiAdapter((self.context, self.request), name='document_generation_helper_view')
1✔
356
        helper_view.pod_template = self.pod_template
1✔
357
        helper_view.output_format = self.output_format
1✔
358
        return helper_view
1✔
359

360
    def get_views_for_appy_renderer(self, generation_context, helper_view):
1✔
361
        views = []
1✔
362
        if 'view' in generation_context:
1✔
363
            # helper_view has maybe changed in generation context getter
364
            views.append(generation_context['view'])
1✔
365
        else:
366
            views.append(helper_view)
×
367

368
        return views
1✔
369

370
    def _set_header_response(self, filename):
1✔
371
        """
372
        Tell the browser that the resulting page contains ODT.
373
        """
374
        response = self.request.RESPONSE
1✔
375
        mimetype = mimetypes.guess_type(filename)[0]
1✔
376
        response.setHeader('Content-type', mimetype)
1✔
377
        response.setHeader(
1✔
378
            'Content-disposition',
379
            u'inline;filename="{}"'.format(filename).encode('utf-8')
380
        )
381

382
    def _check_cyclic_merges(self, pod_template):
1✔
383
        """
384
        Check if the template 'pod_template' has subtemplates referring to each
385
        other in a cyclic way.
386
        """
387

388
        def traverse_check(pod_template, path):
1✔
389

390
            if pod_template in path:
1✔
391
                path.append(pod_template)
1✔
392
                start_cycle = path.index(pod_template)
1✔
393
                start_msg = ' --> '.join(
1✔
394
                    ['"{}" {}'.format(t.Title(), '/'.join(t.getPhysicalPath())) for t in path[:start_cycle]]
395
                )
396
                cycle_msg = ' <--> '.join(
1✔
397
                    ['"{}" {}'.format(t.Title(), '/'.join(t.getPhysicalPath())) for t in path[start_cycle:]]
398
                )
399
                msg = '{} -> CYCLE:\n{}'.format(start_msg, cycle_msg)
1✔
400
                raise CyclicMergeTemplatesException(msg)
1✔
401

402
            new_path = list(path)
1✔
403
            new_path.append(pod_template)
1✔
404

405
            sub_templates = pod_template.get_templates_to_merge()
1✔
406

407
            for name, (sub_template, do_rendering) in iter(sub_templates.items()):
1✔
408
                traverse_check(sub_template, new_path)
1✔
409

410
        traverse_check(pod_template, [])
1✔
411

412

413
class PersistentDocumentGenerationView(DocumentGenerationView):
1✔
414
    """
415
    Persistent document generation view.
416
    """
417

418
    def __call__(self, template_uid='', output_format='', generated_doc_title=''):
1✔
419
        self.generated_doc_title = generated_doc_title
1✔
420
        self.pod_template, self.output_format = self._get_base_args(template_uid, output_format)
1✔
421
        persisted_doc = self.generate_persistent_doc(self.pod_template, self.output_format)
1✔
422
        self.redirects(persisted_doc)
1✔
423

424
    def add_mailing_infos(self, doc, gen_context):
1✔
425
        """ store mailing informations on generated doc """
426
        annot = IAnnotations(doc)
1✔
427
        if 'mailed_data' in gen_context or 'mailing_list' in gen_context:
1✔
428
            annot['documentgenerator'] = {'need_mailing': False, 'template_uid': self.pod_template.UID()}
1✔
429
        else:
430
            annot['documentgenerator'] = {'need_mailing': True, 'template_uid': self.pod_template.UID(),
1✔
431
                                          'output_format': self.output_format, 'context_uid': self.context.UID()}
432

433
    def _get_title(self, doc_name, gen_context):
1✔
434
        splitted_name = doc_name.split('.')
1✔
435
        title = self.generated_doc_title or self.pod_template.title
1✔
436
        extension = splitted_name[-1]
1✔
437
        return safe_unicode(title), extension
1✔
438

439
    def generate_persistent_doc(self, pod_template, output_format):
1✔
440
        """
441
        Generate a document of format 'output_format' from the template
442
        'pod_template' and persist it by creating a File containing the
443
        generated document on the current context.
444
        """
445

446
        doc, doc_name, gen_context = self._generate_doc(pod_template, output_format)
1✔
447

448
        title, extension = self._get_title(doc_name, gen_context)
1✔
449

450
        factory = queryAdapter(self.context, IDocumentFactory)
1✔
451

452
        #  Bypass any File creation permission of the user. If the user isnt
453
        #  supposed to save generated document on the site, then its the permission
454
        #  to call the generation view that should be changed.
455
        with api.env.adopt_roles(['Manager']):
1✔
456
            persisted_doc = factory.create(doc_file=doc, title=title, extension=extension)
1✔
457

458
        # store informations on persisted doc
459
        self.add_mailing_infos(persisted_doc, gen_context)
1✔
460

461
        return persisted_doc
1✔
462

463
    def redirects(self, persisted_doc):
1✔
464
        """
465
        Redirects to the created document.
466
        """
467
        if HAS_PLONE_5_AND_MORE:
1✔
468
            filename = persisted_doc.file.filename
1✔
469
        else:
470
            filename = persisted_doc.getFile().filename
×
471
        self._set_header_response(filename)
1✔
472
        response = self.request.response
1✔
473
        return response.redirect(persisted_doc.absolute_url() + '/external_edit')
1✔
474

475
    def mailing_related_generation_context(self, helper_view, gen_context):
1✔
476
        """
477
            Add mailing related information in generation context
478
        """
479
        # We add mailed_data if we have only one element in mailing list
480
        mailing_list = helper_view.mailing_list(gen_context)
1✔
481
        if len(mailing_list) == 0:
1✔
482
            utils.update_dict_with_validation(gen_context, {'mailed_data': None},
×
483
                                              _("Error when merging mailed_data in generation context"))
484
        elif len(mailing_list) == 1:
1✔
485
            utils.update_dict_with_validation(gen_context, {'mailed_data': mailing_list[0]},
1✔
486
                                              _("Error when merging mailed_data in generation context"))
487

488
    def _get_generation_context(self, helper_view, pod_template):
1✔
489
        """ """
490
        gen_context = super(PersistentDocumentGenerationView, self)._get_generation_context(helper_view, pod_template)
1✔
491
        self.mailing_related_generation_context(helper_view, gen_context)
1✔
492
        return gen_context
1✔
493

494

495
class MailingLoopPersistentDocumentGenerationView(PersistentDocumentGenerationView):
1✔
496
    """
497
        Mailing persistent document generation view.
498
        This view use a MailingLoopTemplate to loop on a document when replacing some variables in.
499
    """
500

501
    def __call__(self, document_uid='', document_url_path='', generated_doc_title=''):
1✔
502
        self.generated_doc_title = generated_doc_title
1✔
503
        document_uid = document_uid or self.request.get('document_uid', '')
1✔
504
        document_url_path = document_url_path or self.request.get('document_url_path', '')
1✔
505
        if not document_uid and not document_url_path:
1✔
506
            raise Exception("No 'document_uid' or 'url_path' found to generate this document")
×
507
        elif document_url_path:
1✔
508
            site = api.portal.get()
1✔
509
            self.document = site.restrictedTraverse(document_url_path)
1✔
510
        else:
511
            self.document = uuidToObject(document_uid)
1✔
512
        if not self.document:
1✔
513
            raise Exception("Cannot find document with UID '{0}'".format(document_uid))
×
514
        self.pod_template, self.output_format = self._get_base_args('', '')
1✔
515
        persisted_doc = self.generate_persistent_doc(self.pod_template, self.output_format)
1✔
516
        self.redirects(persisted_doc)
1✔
517

518
    def _get_base_args(self, template_uid, output_format):
1✔
519
        annot = IAnnotations(self.document).get('documentgenerator', '')
1✔
520
        if not annot or 'template_uid' not in annot:
1✔
521
            raise Exception("Cannot find 'template_uid' on document '{0}'".format(self.document.absolute_url()))
×
522
        self.orig_template = self.get_pod_template(annot['template_uid'])
1✔
523
        if (not base_hasattr(self.orig_template, 'mailing_loop_template') or
1✔
524
                not self.orig_template.mailing_loop_template):
525
            raise Exception("Cannot find 'mailing_loop_template' on template '{0}'".format(
×
526
                self.orig_template.absolute_url()))
527
        loop_template = self.get_pod_template(self.orig_template.mailing_loop_template)
1✔
528

529
        if 'output_format' not in annot:
1✔
530
            raise Exception("No 'output_format' found to generate this document")
×
531
        return loop_template, annot['output_format']
1✔
532

533
    def mailing_related_generation_context(self, helper_view, gen_context):
1✔
534
        """
535
            Add mailing related information in generation context
536
        """
537
        # We do nothing here because we have to call mailing_list after original template context variable inclusion
538

539
    def _get_generation_context(self, helper_view, pod_template):
1✔
540
        """ """
541
        gen_context = super(MailingLoopPersistentDocumentGenerationView, self). \
1✔
542
            _get_generation_context(helper_view, pod_template)
543
        # add variable context from original template
544
        utils.update_dict_with_validation(gen_context, self._get_context_variables(self.orig_template),
1✔
545
                                          _("Error when merging context_variables in generation context"))
546
        # add mailing list in generation context
547
        dic = {'mailing_list': helper_view.mailing_list(gen_context), 'mailed_doc': self.document}
1✔
548
        utils.update_dict_with_validation(gen_context, dic,
1✔
549
                                          _("Error when merging mailing_list in generation context"))
550
        return gen_context
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc