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

zopefoundation / DocumentTemplate / 16248906082

17 Mar 2025 07:56AM UTC coverage: 85.158% (-0.2%) from 85.31%
16248906082

push

github

web-flow
Update Python version support. (#79)

* Drop support for Python 3.8.

709 of 976 branches covered (72.64%)

Branch coverage included in aggregate %.

2 of 6 new or added lines in 5 files covered. (33.33%)

5 existing lines in 3 files now uncovered.

3273 of 3700 relevant lines covered (88.46%)

0.88 hits per line

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

68.22
/src/DocumentTemplate/DT_Var.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE.
11
#
12
##############################################################################
13
"""Variable insertion parameters
14

15
    When inserting variables, parameters may be specified to
16
    control how the data will be formatted.  In HTML source, the
17
    'fmt' parameter is used to specify a C-style or custom format
18
    to be used when inserting an object.  In EPFS source, the 'fmt'
19
    parameter is only used for custom formats, a C-style format is
20
    specified after the closing parenthesis.
21

22
    Custom formats
23

24
       A custom format is used when outputing user-defined
25
       objects.  The value of a custom format is a method name to
26
       be invoked on the object being inserted.  The method should
27
       return an object that, when converted to a string, yields
28
       the desired text.  For example, the DTML code::
29

30
          <dtml-var date fmt=DayOfWeek>
31

32
       Inserts the result of calling the method 'DayOfWeek' of the
33
       object bound to the variable 'date', with no arguments.
34

35
       In addition to object methods, serveral additional custom
36
       formats are available:
37

38
           'whole-dollars' -- Show a numeric value with a dollar symbol.
39

40
           'dollars-and-cents' -- Show a numeric value with a dollar
41
             symbol and two decimal places.
42

43
           'collection-length' -- Get the length of a collection of objects.
44

45
       Note that when using the EPFS source format, both a
46
       C-style and a custom format may be provided.  In this case,
47
       the C-Style format is applied to the result of calling
48
       the custom formatting method.
49

50
    Null values and missing variables
51

52
       In some applications, and especially in database applications,
53
       data variables may alternate between "good" and "null" or
54
       "missing" values.  A format that is used for good values may be
55
       inappropriate for null values.  For this reason, the 'null'
56
       parameter can be used to specify text to be used for null
57
       values.  Null values are defined as values that:
58

59
         - Cannot be formatted with the specified format, and
60

61
         - Are either the special Python value 'None' or
62
           are false and yield an empty string when converted to
63
           a string.
64

65
       For example, when showing a monitary value retrieved from a
66
       database that is either a number or a missing value, the
67
       following variable insertion might be used::
68

69
           <dtml-var cost fmt="$%.2d" null=\'n/a\'>
70

71
       Missing values are providing for variables which are not
72
       present in the name space, rather than raising an NameError,
73
       you could do this:
74

75
           <dtml-var cost missing=0>
76

77
       and in this case, if cost was missing, it would be set to 0.
78
       In the case where you want to deal with both at the same time,
79
       you can use 'default':
80

81
           <dtml-var description default=''>
82

83
       In this case, it would use '' if the value was null or if the
84
       variable was missing.
85

86
    String manipulation
87

88
       A number of special attributes are provided to transform the
89
       value after formatting has been applied.  These parameters
90
       are supplied without arguments.
91

92
       'lower' --  cause all upper-case letters to be converted to lower case.
93

94
       'upper' --  cause all upper-case letters to be converted to lower case.
95

96
       'capitalize' -- cause the first character of the inserted value
97
       to be converted to upper case.
98

99
       'spacify' -- cause underscores in the inserted value to be
100
       converted to spaces.
101

102
       'thousands_commas' -- cause commas to be inserted every three
103
       digits to the left of a decimal point in values containing
104
       numbers.  For example, the value, "12000 widgets" becomes
105
       "12,000 widgets".
106

107
       'html_quote' -- convert characters that have special meaning
108
       in HTML to HTML character entities.
109

110
       'url_quote' -- convert characters that have special meaning
111
       in URLS to HTML character entities using decimal values.
112

113
       'url_quote_plus' -- like url_quote but also replace blank
114
       space characters with '+'. This is needed for building
115
       query strings in some cases.
116

117
       'url_unquote' -- convert HTML character entities in strings
118
       back to their real values.
119

120
       'url_unquote_plus' -- like url_unquote, but also
121
       replace '+' characters with spaces.
122

123
       'sql_quote' -- Convert single quotes to pairs of single
124
       quotes. This is needed to safely include values in
125
       Standard Query Language (SQL) strings.
126

127
       'newline_to_br' -- Convert newlines and carriage-return and
128
       newline combinations to break tags.
129

130
       'url' -- Get the absolute URL of the object by calling it\'s
131
       'absolute_url' method, if it has one.
132

133
    Truncation
134

135
       The attributes 'size' and 'etc'  can be used to truncate long
136
       strings.  If the 'size' attribute is specified, the string to
137
       be inserted is truncated at the given length.  If a space
138
       occurs in the second half of the truncated string, then the
139
       string is further truncated to the right-most space.  After
140
       truncation, the value given for the 'etc' attribute is added to
141
       the string.  If the 'etc' attribute is not provided, then '...'
142
       is used.  For example, if the value of spam is
143
       '"blah blah blah blah"', then the tag
144
       '<dtml-var spam size=10>' inserts '"blah blah ..."'.
145

146

147
Evaluating expressions without rendering results
148

149
   A 'call' tag is provided for evaluating named objects or expressions
150
   without rendering the result.
151
"""
152

153
import logging
1✔
154
import re
1✔
155
import sys
1✔
156
import urllib.parse
1✔
157

158
from AccessControl.tainted import TaintedString
1✔
159
from Acquisition import aq_base
1✔
160
from zope.structuredtext.document import DocumentWithImages
1✔
161

162
# for import by other modules, dont remove!
163
from .DT_Util import name_param
1✔
164
from .DT_Util import parse_params
1✔
165
from .html_quote import html_quote
1✔
166
from .ustr import ustr
1✔
167

168

169
logger = logging.getLogger('DocumentTemplate')
1✔
170

171

172
class Var:
1✔
173
    name = 'var'
1✔
174
    expr = None
1✔
175

176
    def __init__(self, args, fmt='s', encoding=None):
1✔
177
        if args[:4] == 'var ':
1!
178
            args = args[4:]
×
179
        args = parse_params(args, name='', lower=1, upper=1, expr='',
1✔
180
                            capitalize=1, spacify=1, null='', fmt='s',
181
                            size=0, etc='...', thousands_commas=1,
182
                            html_quote=1, url_quote=1, sql_quote=1,
183
                            url_quote_plus=1, url_unquote=1,
184
                            url_unquote_plus=1, missing='',
185
                            newline_to_br=1, url=1)
186
        self.args = args
1✔
187
        self.encoding = encoding
1✔
188

189
        self.modifiers = tuple(
1✔
190
            map(lambda t: t[1],
191
                filter(lambda m, args=args, used=args.__contains__:
192
                       used(m[0]) and args[m[0]],
193
                       modifiers)))
194

195
        name, expr = name_param(args, 'var', 1)
1✔
196

197
        self.__name__, self.expr = name, expr
1✔
198
        self.fmt = fmt
1✔
199

200
        if len(args) == 1 and fmt == 's':
1✔
201
            if expr is None:
1✔
202
                expr = name
1✔
203
            else:
204
                expr = expr.eval
1✔
205
            self.simple_form = ('v', expr)
1✔
206
        elif len(args) == 2 and fmt == 's' and 'html_quote' in args:
1✔
207
            if expr is None:
1!
208
                expr = name
1✔
209
            else:
210
                expr = expr.eval
×
211
            self.simple_form = ('v', expr, 'h')
1✔
212

213
    def render(self, md):
1✔
214
        args = self.args
1✔
215
        name = self.__name__
1✔
216

217
        val = self.expr
1✔
218

219
        if val is None:
1✔
220
            if name in md:
1!
221
                if 'url' in args:
1!
222
                    val = md.getitem(name, 0)
×
223
                    val = val.absolute_url()
×
224
                else:
225
                    val = md[name]
1✔
226
            else:
227
                if 'missing' in args:
×
228
                    return args['missing']
×
229
                else:
230
                    raise KeyError(name)
×
231
        else:
232
            val = val.eval(md)
1✔
233
            if 'url' in args:
1!
234
                val = val.absolute_url()
×
235

236
        __traceback_info__ = name, val, args
1✔
237

238
        if 'null' in args and not val and val != 0:
1✔
239
            # check for null (false but not zero, including None, [], '')
240
            return args['null']
1✔
241

242
        # handle special formats defined using fmt= first
243
        if 'fmt' in args:
1✔
244
            _get = getattr(md, 'guarded_getattr', None)
1✔
245
            if _get is None:
1✔
246
                _get = getattr
1✔
247

248
            fmt = args['fmt']
1✔
249
            if 'null' in args and not val and val != 0:
1!
250
                try:
×
251
                    if hasattr(val, fmt):
×
252
                        val = _get(val, fmt)()
×
253
                    elif fmt in special_formats:
×
254
                        if fmt == 'html-quote' and \
×
255
                           isinstance(val, TaintedString):
256
                            # TaintedStrings will be quoted by default, don't
257
                            # double quote.
258
                            pass
×
259
                        else:
260
                            val = special_formats[fmt](val, name, md)
×
261
                    elif fmt == '':
×
262
                        val = ''
×
263
                    else:
264
                        if isinstance(val, TaintedString):
×
265
                            val = TaintedString(fmt % val)
×
266
                        else:
267
                            val = fmt % val
×
268
                except Exception:
×
269
                    # Not clear which specific error has to be caught.
270
                    t, v = sys.exc_type, sys.exc_value
×
271
                    if hasattr(sys, 'exc_info'):
×
272
                        t, v = sys.exc_info()[:2]
×
273
                    if val is None or not str(val):
×
274
                        return args['null']
×
275
                    raise t(v)
×
276

277
            else:
278
                # We duplicate the code here to avoid exception handler
279
                # which tends to screw up stack or leak
280
                if hasattr(val, fmt):
1✔
281
                    val = _get(val, fmt)()
1✔
282
                elif fmt in special_formats:
1✔
283
                    if fmt == 'html-quote' and \
1!
284
                       isinstance(val, TaintedString):
285
                        # TaintedStrings will be quoted by default, don't
286
                        # double quote.
287
                        pass
×
288
                    else:
289
                        val = special_formats[fmt](val, name, md)
1✔
290
                elif fmt == '':
1!
291
                    val = ''
×
292
                else:
293
                    if isinstance(val, TaintedString):
1!
294
                        val = TaintedString(fmt % val)
×
295
                    else:
296
                        val = fmt % val
1✔
297

298
        # finally, pump it through the actual string format...
299
        fmt = self.fmt
1✔
300
        if fmt == 's':
1!
301
            # Keep tainted strings as tainted strings here.
302
            if not isinstance(val, TaintedString):
1!
303
                val = ustr(val)
1✔
304
        else:
305
            # Keep tainted strings as tainted strings here.
306
            wastainted = 0
×
307
            if isinstance(val, TaintedString):
×
308
                wastainted = 1
×
309
            val = ('%' + self.fmt) % (val,)
×
310
            if wastainted and '<' in val:
×
311
                val = TaintedString(val)
×
312

313
        # next, look for upper, lower, etc
314
        for f in self.modifiers:
1✔
315
            if f.__name__ == 'html_quote' and isinstance(val, TaintedString):
1!
316
                # TaintedStrings will be quoted by default, don't double quote.
317
                continue
×
318
            val = f(val)
1✔
319

320
        if 'size' in args:
1✔
321
            size = args['size']
1✔
322
            try:
1✔
323
                size = int(size)
1✔
324
            except Exception:
×
325
                raise ValueError(
×
326
                    'a <code>size</code> attribute was used in a '
327
                    '<code>var</code> tag with a non-integer value.')
328
            if len(val) > size:
1!
329
                val = val[:size]
1✔
330
                l_ = val.rfind(' ')
1✔
331
                if l_ > size / 2:
1!
332
                    val = val[:l_ + 1]
×
333
                if 'etc' in args:
1!
334
                    l_ = args['etc']
×
335
                else:
336
                    l_ = '...'
1✔
337
                val = val + l_
1✔
338

339
        if isinstance(val, TaintedString):
1!
340
            val = val.quoted()
×
341

342
        return val
1✔
343

344
    __call__ = render
1✔
345

346

347
class Call:
1✔
348

349
    name = 'call'
1✔
350
    expr = None
1✔
351

352
    def __init__(self, args, encoding=None):
1✔
353
        args = parse_params(args, name='', expr='')
1✔
354
        name, expr = name_param(args, 'call', 1)
1✔
355
        if expr is None:
1!
356
            expr = name
×
357
        else:
358
            expr = expr.eval
1✔
359
        self.simple_form = ('i', expr, None)
1✔
360
        self.encoding = encoding
1✔
361

362

363
def url_quote(v, name='(Unknown name)', md={}):
1✔
364
    if isinstance(v, bytes):
1✔
365
        return urllib.parse.quote(v.decode('utf-8')).encode('utf-8')
1✔
366
    return urllib.parse.quote(str(v))
1✔
367

368

369
def url_quote_plus(v, name='(Unknown name)', md={}):
1✔
370
    if isinstance(v, bytes):
1✔
371
        return urllib.parse.quote_plus(v.decode('utf-8')).encode('utf-8')
1✔
372
    return urllib.parse.quote_plus(str(v))
1✔
373

374

375
def url_unquote(v, name='(Unknown name)', md={}):
1✔
376
    if isinstance(v, bytes):
1✔
377
        return urllib.parse.unquote(v.decode('utf-8')).encode('utf-8')
1✔
378
    return urllib.parse.unquote(str(v))
1✔
379

380

381
def url_unquote_plus(v, name='(Unknown name)', md={}):
1✔
382
    if isinstance(v, bytes):
1✔
383
        return urllib.parse.unquote_plus(v.decode('utf-8')).encode('utf-8')
1✔
384
    return urllib.parse.unquote_plus(str(v))
1✔
385

386

387
def newline_to_br(v, name='(Unknown name)', md={}):
1✔
388
    # Unsafe data is explicitly quoted here; we don't expect this to be HTML
389
    # quoted later on anyway.
390
    if isinstance(v, TaintedString):
1✔
391
        v = v.quoted()
1✔
392
    v = ustr(v)
1✔
393
    v = v.replace('\r', '')
1✔
394
    v = v.replace('\n', '<br />\n')
1✔
395
    return v
1✔
396

397

398
def whole_dollars(v, name='(Unknown name)', md={}):
1✔
399
    try:
1✔
400
        return "$%d" % v
1✔
401
    except Exception:
1✔
402
        return ''
1✔
403

404

405
def dollars_and_cents(v, name='(Unknown name)', md={}):
1✔
406
    try:
1✔
407
        return "$%.2f" % v
1✔
408
    except Exception:
1✔
409
        return ''
1✔
410

411

412
def thousands_commas(v, name='(Unknown name)', md={},
1✔
413
                     thou=re.compile(
414
                         r"([0-9])([0-9][0-9][0-9]([,.]|$))").search):
415
    v = str(v)
1✔
416
    vl = v.split('.')
1✔
417
    if not vl:
1!
418
        return v
×
419
    v = vl[0]
1✔
420
    del vl[0]
1✔
421
    if vl:
1✔
422
        s = '.' + '.'.join(vl)
1✔
423
    else:
424
        s = ''
1✔
425
    mo = thou(v)
1✔
426
    while mo is not None:
1✔
427
        l_ = mo.start(0)
1✔
428
        v = v[:l_ + 1] + ',' + v[l_ + 1:]
1✔
429
        mo = thou(v)
1✔
430
    return v + s
1✔
431

432

433
def whole_dollars_with_commas(v, name='(Unknown name)', md={}):
1✔
434
    try:
1✔
435
        v = "$%d" % v
1✔
436
    except Exception:
1✔
437
        v = ''
1✔
438
    return thousands_commas(v)
1✔
439

440

441
def dollars_and_cents_with_commas(v, name='(Unknown name)', md={}):
1✔
442
    try:
1✔
443
        v = "$%.2f" % v
1✔
444
    except Exception:
1✔
445
        v = ''
1✔
446
    return thousands_commas(v)
1✔
447

448

449
def len_format(v, name='(Unknown name)', md={}):
1✔
450
    return str(len(v))
×
451

452

453
def len_comma(v, name='(Unknown name)', md={}):
1✔
454
    return thousands_commas(str(len(v)))
×
455

456

457
def restructured_text(v, name='(Unknown name)', md={}):
1✔
458
    try:
1✔
459
        from docutils.core import publish_string
1✔
NEW
460
    except ModuleNotFoundError:
×
UNCOV
461
        logger.info('The docutils package is not available, therefore '
×
462
                    'the DT_Var.restructured_text function returns None.')
UNCOV
463
        return None
×
464

465
    if isinstance(v, (str, bytes)):
1!
466
        data = v
1✔
467
    elif aq_base(v).meta_type in ['DTML Document', 'DTML Method']:
×
468
        data = aq_base(v).read_raw()
×
469
    else:
470
        data = str(v)
×
471

472
    # Override some docutils default settings
473
    # The default for output_encoding is UTF8 already, the settings
474
    # override acts as a reminder.
475
    rest_settings_overrides = {'file_insertion_enabled': False,
1✔
476
                               'raw_enabled': False,
477
                               'output_encoding': 'UTF-8'}
478

479
    html_bytes = publish_string(data,
1✔
480
                                writer_name='html',
481
                                settings_overrides=rest_settings_overrides)
482

483
    # The formatting methods are expected to return native strings.
484
    return html_bytes.decode('UTF-8')
1✔
485

486

487
def structured_text(v, name='(Unknown name)', md={}):
1✔
488
    from zope.structuredtext.html import HTML
×
489

490
    if isinstance(v, str):
×
491
        txt = v
×
492
    elif aq_base(v).meta_type in ['DTML Document', 'DTML Method']:
×
493
        txt = aq_base(v).read_raw()
×
494
    else:
495
        txt = str(v)
×
496

497
    level = 3
×
498
    try:
×
499
        from App.config import getConfiguration
×
NEW
500
    except ModuleNotFoundError:
×
UNCOV
501
        pass
×
502
    else:
503
        level = getConfiguration().structured_text_header_level
×
504

505
    doc = DocumentWithImages()(txt)
×
506
    return HTML()(doc, level, header=False)
×
507

508

509
def sql_quote(v, name='(Unknown name)', md={}):
1✔
510
    """Quote single quotes in a string by doubling them.
511

512
    This is needed to securely insert values into sql
513
    string literals in templates that generate sql.
514
    """
515
    if isinstance(v, bytes):
1✔
516
        v = v.decode('UTF-8')
1✔
517

518
    # Remove bad characters
519
    for char in ('\x00', '\x1a', '\r'):
1✔
520
        v = v.replace(char, '')
1✔
521

522
    # Double untrusted characters to make them harmless.
523
    for char in ("'",):
1✔
524
        v = v.replace(char, char * 2)
1✔
525

526
    return v
1✔
527

528

529
special_formats = {
1✔
530
    'whole-dollars': whole_dollars,
531
    'dollars-and-cents': dollars_and_cents,
532
    'collection-length': len_format,
533
    'structured-text': structured_text,
534
    'restructured-text': restructured_text,
535

536
    # The rest are deprecated:
537
    'sql-quote': sql_quote,
538
    'html-quote': html_quote,
539
    'url-quote': url_quote,
540
    'url-quote-plus': url_quote_plus,
541
    'url-unquote': url_unquote,
542
    'url-unquote-plus': url_unquote_plus,
543
    'multi-line': newline_to_br,
544
    'comma-numeric': thousands_commas,
545
    'dollars-with-commas': whole_dollars_with_commas,
546
    'dollars-and-cents-with-commas': dollars_and_cents_with_commas,
547
}
548

549

550
def lower(val):
1✔
551
    return val.lower()
×
552

553

554
def upper(val):
1✔
555
    return val.upper()
×
556

557

558
def capitalize(val):
1✔
559
    return val.capitalize()
1✔
560

561

562
def spacify(val):
1✔
563
    if val.find('_') >= 0:
1!
564
        val = val.replace('_', ' ')
1✔
565
    return val
1✔
566

567

568
modifiers = (
1✔
569
    html_quote, url_quote, url_quote_plus, url_unquote,
570
    url_unquote_plus, newline_to_br,
571
    lower, upper, capitalize, spacify,
572
    thousands_commas, sql_quote, url_unquote, url_unquote_plus,
573
)
574
modifiers = list(map(lambda f: (f.__name__, f), modifiers))
1✔
575

576

577
class Comment:
1✔
578
    """Comments
579

580
    The 'comment' tag can be used to simply include comments
581
    in DTML source.
582

583
    For example::
584

585
      <!--#comment-->
586

587
        This text is not rendered.
588

589
      <!--#/comment-->
590
    """
591
    name = 'comment'
1✔
592
    blockContinuations = ()
1✔
593

594
    def __init__(self, args, fmt='', encoding=None):
1✔
595
        pass
×
596

597
    def render(self, md):
1✔
598
        return ''
×
599

600
    __call__ = render
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

© 2025 Coveralls, Inc