Skip to content

Commit f9691e8

Browse files
committed
issue2551270 - Better templating support for JavaScript
Add (templating) utils.readfile(file, optional=False) and utils.expandfile(file, token_dict=None, optional=False). Allows reading an external file (e.g. JavaScript) and inserting it using tal:contents or equivalent jinja function. expandfile allows setting a dictionary and tokens in the file of the form "%(token_name)s" will be replaced in the file with the values from the dict. See method doc blocks or reference.txt for more info. Also reordered table in references.txt to be case sensitive alphabetic. Added a paragraph on using python's help() to get method/function/... documention blocks. in templating.py _find method. Added explicit return None calls to all code paths. Also added internationalization method to the TemplatingUtils class. Fixed use of 'property' hiding python builtin of same name. Added tests for new TemplatingUtils framework to use for testing existing utils.
1 parent 4eb0a3b commit f9691e8

File tree

4 files changed

+389
-5
lines changed

4 files changed

+389
-5
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ Features:
146146
- issue2551212 - wsgi performance improvement feature added in 2.2.0
147147
is active by default. Can be turned off if needed. See upgrading.txt
148148
for info. (John Rouillard)
149+
- issue2551270 - Better templating support for JavaScript. Add
150+
utils.readfile(file, optional=False) and utils.expandfile(file,
151+
token_dict=None, optional=False). Allows reading an external file
152+
(e.g. JavaScript) and inserting it using tal:contents or equivalent
153+
jinja function. expandfile allows setting a dictionary and tokens in
154+
the file of the form "%(token_name)s" will be replaced in the file
155+
with the values from the dict. (John Rouillard)
149156

150157
2023-07-13 2.3.0
151158

doc/reference.txt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3596,14 +3596,37 @@ with additional methods by extensions_.
35963596
Method Description
35973597
=============== ========================================================
35983598
Batch return a batch object using the supplied list
3599-
url_quote quote some text as safe for a URL (ie. space, %, ...)
3599+
anti_csrf_nonce returns the random nonce generated for this session
3600+
expandfile load a file into a template and expand
3601+
'%(tokenname)s' in the file using
3602+
values from the supplied dictionary.
36003603
html_quote quote some text as safe in HTML (ie. <, >, ...)
36013604
html_calendar renders an HTML calendar used by the
36023605
``_generic.calendar.html`` template (itself invoked by
36033606
the popupCalendar DateHTMLProperty method
3604-
anti_csrf_nonce returns the random nonce generated for this session
3607+
readfile read Javascript or other content in an external
3608+
file into the template.
3609+
url_quote quote some text as safe for a URL (ie. space, %, ...)
36053610
=============== ========================================================
36063611

3612+
Additional info can be obtained by starting ``python`` with the
3613+
``roundup`` subdirectory on your PYTHONPATH and using the Python help
3614+
function like::
3615+
3616+
>>> from roundup.cgi.templating import TemplatingUtils
3617+
>>> help(TemplatingUtils.readfile)
3618+
Help on function readfile in module roundup.cgi.templating:
3619+
3620+
readfile(self, name, optional=False)
3621+
Read an file in the template directory.
3622+
3623+
Used to inline file content into a template. If file
3624+
is not found in template directory, reports an error
3625+
to the user unless optional=True. Then it returns an
3626+
empty string. Useful inlining JavaScript kept in an
3627+
external file where you can use linters/minifiers and
3628+
3629+
(Note: ``>>>``` is the Python REPL prompt. Don't type the ``>>>```.)
36073630

36083631
Batching
36093632
::::::::

roundup/cgi/templating.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import calendar
2323
import csv
24+
import logging
2425
import os.path
2526
import re
2627
import textwrap
@@ -52,6 +53,7 @@
5253
except ImportError:
5354
from itertools import izip_longest as zip_longest
5455

56+
logger = logging.getLogger('roundup.template')
5557

5658
# List of schemes that are not rendered as links in rst and markdown.
5759
_disable_url_schemes = ['javascript', 'data']
@@ -329,9 +331,10 @@ def _find(self, name):
329331
src = os.path.join(realsrc, f)
330332
realpath = os.path.realpath(src)
331333
if not realpath.startswith(realsrc):
332-
return # will raise invalid template
334+
return None # will raise invalid template
333335
if os.path.exists(src):
334336
return (src, f)
337+
return None
335338

336339
def check(self, name):
337340
return bool(self._find(name))
@@ -3569,6 +3572,7 @@ class TemplatingUtils:
35693572
"""
35703573
def __init__(self, client):
35713574
self.client = client
3575+
self._ = self.client._
35723576

35733577
def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
35743578
return Batch(self.client, sequence, size, start, end, orphan,
@@ -3619,7 +3623,7 @@ def html_calendar(self, request):
36193623
display = request.form.getfirst("display", date_str)
36203624
template = request.form.getfirst("@template", "calendar")
36213625
form = request.form.getfirst("form")
3622-
property = request.form.getfirst("property")
3626+
aproperty = request.form.getfirst("property")
36233627
curr_date = ""
36243628
try:
36253629
# date_str and display can be set to an invalid value
@@ -3656,7 +3660,7 @@ def html_calendar(self, request):
36563660
res = []
36573661

36583662
base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
3659-
(request.classname, template, property, form, curr_date)
3663+
(request.classname, template, aproperty, form, curr_date)
36603664

36613665
# navigation
36623666
# month
@@ -3718,6 +3722,92 @@ def html_calendar(self, request):
37183722
res.append('</table></td></tr></table>')
37193723
return "\n".join(res)
37203724

3725+
def readfile(self, name, optional=False):
3726+
"""Used to inline a file from the template directory.
3727+
3728+
Used to inline file content into a template. If file
3729+
is not found in the template directory and
3730+
optional=False, it reports an error to the user via a
3731+
NoTemplate exception. If optional=True it returns an
3732+
empty string when it can't find the file.
3733+
3734+
Useful for inlining JavaScript kept in an external
3735+
file where you can use linters/minifiers and other
3736+
tools on it.
3737+
3738+
A TAL example:
3739+
3740+
<script tal:attributes="nonce request/client/client_nonce"
3741+
tal:content="python:utils.readfile('mylibrary.js')"></script>
3742+
3743+
This method does not expands any tokens in the file.
3744+
See expandfile() for replacing tokens in the file.
3745+
"""
3746+
file_result = self.client.instance.templates._find(name)
3747+
3748+
if file_result is None:
3749+
if optional:
3750+
return ""
3751+
template_name = self.client.selectTemplate(
3752+
self.client.classname, self.client.template)
3753+
raise NoTemplate(self._(
3754+
"Unable to read or expand file '%(name)s' "
3755+
"in template '%(template)s'.") % {
3756+
"name": name, 'template': template_name})
3757+
3758+
fullpath, name = file_result
3759+
with open(fullpath) as f:
3760+
contents = f.read()
3761+
return contents
3762+
3763+
def expandfile(self, name, values=None, optional=False):
3764+
"""Read a file and replace token placeholders.
3765+
3766+
Given a file name and a dict of tokens and
3767+
replacements, read the file from the tracker template
3768+
directory. Then replace all tokens of the form
3769+
'%(token_name)s' with the values in the dict. If the
3770+
values dict is set to None, it acts like
3771+
readfile(). In addition to values passed into the
3772+
method, the value for the tracker base directory taken
3773+
from TRACKER_WEB is available as the 'base' token. The
3774+
client_nonce used for Content Security Policy (CSP) is
3775+
available as 'client_nonce'. If a token is not in the
3776+
dict, an empty string is returned and an error log
3777+
message is logged. See readfile for an usage example.
3778+
"""
3779+
# readfile() raises NoTemplate if optional = false and
3780+
# the file is not found. Returns empty string if file not
3781+
# found and optional = true. File contents otherwise.
3782+
contents = self.readfile(name, optional=optional)
3783+
3784+
if values is None or not contents: # nothing to expand
3785+
return contents
3786+
tokens = {'base': self.client.db.config.TRACKER_WEB,
3787+
'client_nonce': self.client.client_nonce}
3788+
tokens.update(values)
3789+
try:
3790+
return contents % tokens
3791+
except KeyError as e:
3792+
template_name = self.client.selectTemplate(
3793+
self.client.classname, self.client.template)
3794+
fullpath, name = self.client.instance.templates._find(name)
3795+
logger.error(
3796+
"When running expandfile('%(fullpath)s') in "
3797+
"'%(template)s' there was no value for token: '%(token)s'.",
3798+
{'fullpath': fullpath, 'token': e.args[0],
3799+
'template': template_name})
3800+
return ""
3801+
except ValueError as e:
3802+
fullpath, name = self.client.instance.templates._find(name)
3803+
logger.error(self._(
3804+
"Found an incorrect token when expandfile applied "
3805+
"string subsitution on '%(fullpath)s'. "
3806+
"ValueError('%(issue)s') was raised. Check the format "
3807+
"of your named conversion specifiers."),
3808+
{'fullpath': fullpath, 'issue': e.args[0]})
3809+
return ""
3810+
37213811

37223812
class MissingValue(object):
37233813
def __init__(self, description, **kwargs):

0 commit comments

Comments
 (0)