Package turbogears :: Package view :: Module base

Source Code for Module turbogears.view.base

  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  # the gettext imported here can be whatever function returned 
 37  # by the i18n module depending on the user's configuration 
 38  # all the functions will implement the same interface so we don't care 
 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   
69 -def _choose_engine(template):
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 # if a template arg was given we try to find the engine declaration 89 # in it by 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 # Determine the proper content type and charset for the response. 172 # We simply derive the content type from the format here 173 # and use the charset specified in the configuration setting. 174 # This could be improved by also examining the engine and the output. 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
213 -def transform(info, template):
214 """Create ElementTree representation of the output.""" 215 engine, template, enginename = _choose_engine(template) 216 return engine.transform(info, template)
217 218
219 -def loadBaseTemplates():
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
235 -class cycle:
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
244 - def __init__(self, iterable):
245 self._cycle = icycle(iterable)
246
247 - def __str__(self):
248 return self.value.__str__()
249
250 - def __repr__(self):
251 return self.value.__repr__()
252
253 - def next(self):
254 self.value = self._cycle.next() 255 return self.value
256 257
258 -def selector(expression):
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
270 -def checker(expression):
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
282 -def ipeek(iterable):
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
298 -class UserAgent:
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
309 - def __init__(self, useragent=None):
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 # this comes from: 324 # http://developer.apple.com/internet/safari/uamatrix.html 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
343 -def genshi_et(element):
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
360 -def kid_xml(stream):
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
370 -def stdvars():
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
462 -def _get_plugin_options(plugin_name, defaults=None):
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
483 -def _get_genshi_loader_callback(template_filter):
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
490 -def load_engines():
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 # Check if the i18n filter is activated in configuration. 525 # If so we'll need to add it to the Genshi or Kid template filters. 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 # Replace Genshi markup template engine with our own derived class 544 # to support our extensions to the Buffet interface. 545 # This is only a temporary measure. We should try to push our extension 546 # upstream and then require a new Genshi version. 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