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