1 """Template processing for TurboGears view layer.
2
3 The template engines are configured and loaded here and this module provides
4 the generic template rendering function "render", which selects the template
5 engine to use and the appropriate output format, headers and encoding based
6 on the given arguments and the application configuration.
7
8 Also defines the functions and variables that will be available in the
9 template scope and provides a hook for adding additional template variables.
10
11 """
12
13 import sys
14 import re
15 import logging
16
17 from itertools import chain, imap
18 from itertools import cycle as icycle
19 from urllib import quote_plus
20
21 import cherrypy
22 import genshi
23 import pkg_resources
24
25 import turbogears
26 from turbogears import identity, config
27
28
29
30
31 from turbogears.i18n import get_locale, gettext
32
33 from turbogears.util import (Bunch, adapt_call, get_template_encoding_default,
34 get_mime_type_for_format, mime_type_has_charset)
35
36 try:
37 from turbogears.i18n.kidutils import i18n_filter as kid_i18n_filter
38 except ImportError:
39 kid_i18n_filter = None
40
41 try:
42 from genshi.filters import Translator
44 """Callback to add a gettext Translator filter to Genshi templates.
45
46 Private module function not intended to for public use.
47
48 This is called by the Genshi template loader and adds Translator filter
49 as the first in the list of filters as is required for translation to
50 work properly.
51
52 """
53 template.filters.insert(0, Translator(gettext))
54 from genshisupport import TGGenshiTemplatePlugin
55 except ImportError:
56 _genshi_translator_cb = None
57 TGGenshiTemplatePlugin = None
58
59
60 log = logging.getLogger("turbogears.view")
61
62 baseTemplates = []
63 variable_providers = []
64 root_variable_providers = []
65 engines = dict()
66
67
69 """Return template engine for given template name.
70
71 Parses template name from the expose 'template' argument. If the @expose
72 decorator did not contain a template argument, we fetch the default engine
73 info from the configuration file.
74
75 @param template: a template string as seen in the expose decorator.
76 This can be something like "kid:myproj.templates.welcome" or just
77 "myproj.templates.welcome".
78 If a colon is found, then we try to get the engine name from the
79 template string. Else we try to search it from the default engine in
80 the configuration file via the `tg.defaultview` setting.
81 The template name may also be just the name of the template engine,
82 as in @expose("json").
83 @type template: basestring or None
84
85 """
86 if isinstance(template, basestring):
87
88
89 colon = template.find(":")
90 if colon > -1:
91 enginename = template[:colon]
92 template = template[colon+1:]
93
94 else:
95 engine = engines.get(template, None)
96 if engine:
97 return engine, None, template
98 enginename = config.get("tg.defaultview", "genshi")
99
100 else:
101 enginename = config.get("tg.defaultview", "genshi")
102
103 engine = engines.get(enginename, None)
104
105 if not engine:
106 raise KeyError, \
107 "Template engine %s is not installed" % enginename
108
109 return engine, template, enginename
110
111
112 -def render(info, template=None, format=None, headers=None, fragment=False,
113 **options):
114 """Renders data in the desired format.
115
116 @param info: the data itself
117 @type info: dict
118
119 @param template: name of the template to use
120 @type template: string
121
122 @param format: "html", "xml", "text" or "json"
123 @type format: string
124
125 @param headers: for response headers, primarily the content type
126 @type headers: dict
127
128 @param fragment: passed through to tell the template if only a
129 fragment of a page is desired. This is a way to allow
130 xml template engines to generate non valid html/xml
131 because you warn them to not bother about it.
132 @type fragment: bool
133
134 All additional keyword arguments are passed as keyword args to the render
135 method of the template engine.
136
137 """
138 environ = getattr(cherrypy.request, 'wsgi_environ', {})
139 if environ.get('paste.testing', False):
140 cherrypy.request.wsgi_environ['paste.testing_variables']['raw'] = info
141
142 template = format == 'json' and 'json' or info.pop(
143 "tg_template", template)
144
145 if not info.has_key("tg_flash"):
146 if config.get("tg.empty_flash", True):
147 info["tg_flash"] = None
148
149 engine, template, enginename = _choose_engine(template)
150
151 if format:
152 if format == 'plain':
153 if enginename == 'genshi':
154 format = 'text'
155
156 elif format == 'text':
157 if enginename == 'kid':
158 format = 'plain'
159
160 else:
161 format = enginename == 'json' and 'json' or config.get(
162 "%s.outputformat" % enginename,
163 config.get("%s.default_format" % enginename, 'html'))
164
165 if isinstance(headers, dict):
166
167
168
169
170 content_type = headers.get('Content-Type')
171 if not content_type:
172 if format:
173 content_format = format
174 if isinstance(content_format, (tuple, list)):
175 content_format = content_format[0]
176
177 if isinstance(content_format, str):
178 content_format = content_format.split(
179 )[0].split('-' , 1)[0].lower()
180
181 else:
182 content_format = 'html'
183
184 else:
185 content_format = 'html'
186
187 content_type = get_mime_type_for_format(content_format)
188
189 if mime_type_has_charset(
190 content_type) and '; charset=' not in content_type:
191 charset = options.get('encoding',
192 get_template_encoding_default(enginename))
193
194 if charset:
195 content_type += '; charset=' + charset
196
197 headers['Content-Type'] = content_type
198
199 args, kw = adapt_call(engine.render, args=[], kw=dict(
200 info=info, format=format, fragment=fragment, template=template,
201 **options), start=1)
202
203 return engine.render(**kw)
204
205
210
211
213 """Load base templates for use by other templates.
214
215 By listing templates in turbogears.view.baseTemplates,
216 these templates will automatically be loaded so that
217 the "import" statement in a template will work.
218
219 """
220 log.debug("Loading base templates")
221 for template in baseTemplates:
222 engine, template, enginename = _choose_engine(template)
223 if sys.modules.has_key(template):
224 del sys.modules[template]
225 engine.load_template(template)
226
227
229 """Loops forever over an iterator.
230
231 Wraps the itertools.cycle method, but provides a way to get the current
232 value via the 'value' attribute.
233
234 """
235 value = None
236
238 self._cycle = icycle(iterable)
239
242
245
249
250
252 """If the expression is true, return the string 'selected'.
253
254 Useful for HTML <option>s.
255
256 """
257 if expression:
258 return "selected"
259 else:
260 return None
261
262
264 """If the expression is true, return the string "checked".
265
266 This is useful for checkbox inputs.
267
268 """
269 if expression:
270 return "checked"
271 else:
272 return None
273
274
276 """Lets you look at the first item in an iterator.
277
278 This is a good way to verify that the iterator actually contains something.
279 This is useful for cases where you will choose not to display a list or
280 table if there is no data present.
281
282 """
283 iterable = iter(iterable)
284 try:
285 item = iterable.next()
286 return chain([item], iterable)
287 except StopIteration:
288 return None
289
290
292 """Representation of the user's browser.
293
294 Provides information about the type of browser, browser version, etc.
295 This currently contains only the information needed for work thus far
296 (msie, firefox, safari browser types, plus safari version info).
297
298 """
299
300 _re_safari = re.compile(r"Safari/(\d+)")
301
303 self.majorVersion = None
304 self.minorVersion = None
305 if not useragent:
306 useragent = "unknown"
307 if useragent.find("MSIE") > -1:
308 self.browser = "msie"
309 elif useragent.find("Firefox") > -1:
310 self.browser = "firefox"
311 else:
312 isSafari = self._re_safari.search(useragent)
313 if isSafari:
314 self.browser = "safari"
315 build = int(isSafari.group(1))
316
317
318 if build >= 412:
319 self.majorVersion = "2"
320 self.minorVersion = "0"
321 elif build >= 312:
322 self.majorVersion = "1"
323 self.minorVersion = "3"
324 elif build >= 125:
325 self.majorVersion = "1"
326 self.minorVersion = "2"
327 elif build >= 85:
328 self.majorVersion = "1"
329 self.minorVersion = "0"
330 elif useragent == "unknown" or useragent is None:
331 self.browser = "unknown"
332 else:
333 self.browser = "unknown: %s" % useragent
334
335
337 """If this is an ElementTree element, convert it to a markup stream.
338
339 If this is a list, apply this function recursively and chain everything.
340
341 """
342 if hasattr(element, 'tag'):
343 return genshi.input.ET(element)
344
345 elif isinstance(element, list):
346 return chain(*imap(genshi_et, element))
347
348 else:
349 return element
350
351
353 """Create a Bunch of variables that should be available in all templates.
354
355 These variables are:
356
357 checker
358 the checker function
359 config
360 the cherrypy config get function
361 cycle
362 cycle through a set of values
363 errors
364 validation errors
365 identity
366 the current visitor's identity information
367 inputs
368 input values from a form
369 ipeek
370 the ipeek function
371 locale
372 the default locale
373 quote_plus
374 the urllib quote_plus function
375 request
376 the cherrypy request
377 selector
378 the selector function
379 session
380 the current cherrypy.session if the session_filter.on it set
381 in the app.cfg configuration file. If it is not set then session
382 will be None.
383 tg_js
384 the url path to the JavaScript libraries
385 tg_static
386 the url path to the TurboGears static files
387 tg_toolbox
388 the url path to the TurboGears toolbox files
389 tg_version
390 the version number of the running TurboGears instance
391 url
392 the turbogears.url function for creating flexible URLs
393 useragent
394 a UserAgent object with information about the browser
395
396 Additionally, you can add a callable to turbogears.view.variable_providers
397 that can add more variables to this list. The callable will be called with
398 the vars Bunch after these standard variables have been set up.
399
400 """
401 try:
402 useragent = cherrypy.request.headers['User-Agent']
403 useragent = UserAgent(useragent)
404 except Exception:
405 useragent = UserAgent()
406
407 if config.get('session_filter.on', None):
408 session = cherrypy.session
409 else:
410 session = None
411
412 webpath = turbogears.startup.webpath or ''
413 tg_vars = Bunch(
414 checker = checker,
415 config = config.get,
416 cycle = cycle,
417 errors = getattr(cherrypy.request, 'validation_errors', {}),
418 identity = identity.current,
419 inputs = getattr(cherrypy.request, 'input_values', {}),
420 ipeek = ipeek,
421 locale = get_locale(),
422 quote_plus = quote_plus,
423 request = cherrypy.request,
424 selector = selector,
425 session = session,
426 tg_js = '/' + webpath + 'tg_js',
427 tg_static = '/' + webpath + 'tg_static',
428 tg_toolbox = '/' + webpath + 'tg_toolbox',
429 tg_version = turbogears.__version__,
430 url = turbogears.url,
431 useragent = useragent,
432 widgets = '/' + webpath + 'tg_widgets',
433 )
434 for provider in variable_providers:
435 provider(tg_vars)
436 root_vars = dict()
437 root_vars['_'] = gettext
438 for provider in root_variable_providers:
439 provider(root_vars)
440 root_vars['tg'] = tg_vars
441 root_vars['ET'] = genshi_et
442 return root_vars
443
444
446 """Return all options from global config where the first part of the config
447 setting name matches the start of plugin_name.
448
449 Optionally, add default values from passed ``default`` dict where the first
450 part of the key (i.e. everything leading up to the first dot) matches the
451 start of ``plugin_name``. The defaults will be overwritten by the
452 corresponding config settings, if present.
453
454 """
455 if defaults is not None:
456 options = dict((k, v) for k, v in defaults.items()
457 if plugin_name.startswith(k.split('.', 1)[0]))
458 else:
459 options = dict()
460 for k, v in config.config.configMap["global"].items():
461 if plugin_name.startswith(k.split('.', 1)[0]):
462 options[k] = v
463 return options
464
465
467 """Load and initialize all templating engines.
468
469 This is called during startup after the configuration has been loaded.
470 You can call this earlier if you need the engines before startup;
471 the engines will then be reloaded with the custom configuration later.
472
473 """
474 get = config.get
475
476 engine_defaults = {
477 "cheetah.importhooks": False,
478 "cheetah.precompiled": False,
479 "genshi.default_doctype":
480 dict(html='html-strict', xhtml='xhtml-strict', xml=None),
481 "genshi.default_encoding": "utf-8",
482 "genshi.lookup_errors": "strict",
483 "genshi.new_text_syntax": False,
484 "json.assume_encoding": "utf-8",
485 "json.check_circular": True,
486 "json.descent_bases": get("json.descent_bases",
487 get("turbojson.descent_bases", True)),
488 "json.encoding": "utf-8",
489 "json.ensure_ascii": False,
490 "json.sort_keys": False,
491 "kid.assume_encoding": "utf-8",
492 "kid.encoding": "utf-8",
493 "kid.precompiled": False,
494 "kid.sitetemplate": get("kid.sitetemplate",
495 get("tg.sitetemplate", "turbogears.view.templates.sitetemplate")),
496 "mako.directories": [''],
497 "mako.output_encoding": "utf-8"
498 }
499
500
501
502
503 if get("i18n.run_template_filter", False):
504 if _genshi_translator_cb:
505 callback = get("genshi.loader_callback", _genshi_translator_cb)
506 engine_defaults['genshi.loader_callback'] = callback
507 if kid_i18n_filter:
508 kid_filter = get("kid.i18n_filter", kid_i18n_filter)
509 engine_defaults["kid.i18n_filter"] = kid_filter
510 engine_defaults["kid.i18n.run_template_filter"] = True
511
512 for entrypoint in pkg_resources.iter_entry_points(
513 "python.templating.engines"):
514 engine = entrypoint.load()
515 plugin_name = entrypoint.name
516 engine_options = _get_plugin_options(plugin_name, engine_defaults)
517 log.debug("Using options for template engine '%s': %r", plugin_name,
518 engine_options)
519
520
521
522
523 if plugin_name in ('genshi', 'genshi-markup') \
524 and TGGenshiTemplatePlugin:
525 engines[plugin_name] = TGGenshiTemplatePlugin(
526 stdvars, engine_options)
527 else:
528 engines[plugin_name] = engine(stdvars, engine_options)
529