Trees Indices Help
Trac
Package trac :: Package web :: Module chrome

Source Code for Module trac.web.chrome

 1 # -*- coding: utf-8 -*- 
 2 # 
 3 # Copyright (C) 2005-2009 Edgewall Software 
 4 # Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de> 
 5 # All rights reserved. 
 6 # 
 7 # This software is licensed as described in the file COPYING, which 
 8 # you should have received as part of this distribution. The terms 
 9 # are also available at http://trac.edgewall.org/wiki/TracLicense. 
 10 # 
 11 # This software consists of voluntary contributions made by many 
 12 # individuals. For the exact contribution history, see the revision 
 13 # history and logs, available at http://trac.edgewall.org/log/. 
 14 # 
 15 # Author: Christopher Lenz <cmlenz@gmx.de> 
 16 
 17 import datetime 
 18 import itertools 
 19 import os.path  
 20 import pkg_resources 
 21 import pprint 
 22 import re 
 23 try: 
 24  from cStringIO import StringIO 
 25 except ImportError: 
 26  from StringIO import StringIO 
 27 
 28 from genshi import Markup 
 29 from genshi.builder import tag , Element 
 30 from genshi.core  import Attrs, START 
 31 from genshi.filters  import Translator 
 32 from genshi.output import DocType 
 33 from genshi.template import TemplateLoader, MarkupTemplate, NewTextTemplate 
 34 
 35 from trac  import __version__ as VERSION 
 36 from trac .config  import * 
 37 from trac .core  import * 
 38 from trac .env  import IEnvironmentSetupParticipant , ISystemInfoProvider 
 39 from trac .mimeview  import get_mimetype , Context  
 40 from trac .resource  import * 
 41 from trac .util  import compat , get_reporter_id , presentation , get_pkginfo , \ 
 42  pathjoin , translation  
 43 from trac .util .compat  import any, partial 
 44 from trac .util .html  import escape , plaintext  
 45 from trac .util .text  import pretty_size , obfuscate_email_address , \ 
 46  shorten_line , unicode_quote_plus , to_unicode , \ 
 47  javascript_quote , exception_to_unicode  
 48 from trac .util .datefmt  import pretty_timedelta , format_datetime , format_date , \ 
 49  format_time , from_utimestamp , http_date , utc  
 50 from trac .util .translation  import _ , get_available_locales 
 51 from trac .web .api  import IRequestHandler , ITemplateStreamFilter , HTTPNotFound  
 52 from trac .web .href  import Href  
 53 from trac .wiki  import IWikiSyntaxProvider  
 54 from trac .wiki .formatter  import format_to, format_to_html, format_to_oneliner 
 55 
 56 
57 - def add_meta (req, content, http_equiv=None, name=None, scheme=None, lang=None):
58 """Add a `<meta>` tag into the `<head>` of the generated HTML.""" 59 meta = {'content': content, 'http-equiv': http_equiv, 'name': name , 60 'scheme': scheme , 'lang': lang, 'xml:lang': lang} 61 req.chrome .setdefault('metas', []).append(meta)
62 78
79 - def add_stylesheet (req, filename, mimetype='text/css', media=None):
80 """Add a link to a style sheet to the chrome info so that it gets included 81 in the generated HTML page. 82 83 If the filename is absolute (i.e. starts with a slash), the generated link 84 will be based off the application root path. If it is relative, the link 85 will be based off the `/chrome/` path. 86 """ 87 if filename .startswith('common/') and 'htdocs_location' in req.chrome : 88 href = Href (req.chrome ['htdocs_location']) 89 filename = filename [7:] 90 else: 91 href = req.href 92 if not filename .startswith('/'): 93 href = href .chrome 94 add_link (req, 'stylesheet', href (filename ), mimetype=mimetype, media=media)
95
96 - def add_script (req, filename, mimetype='text/javascript'):
97 """Add a reference to an external javascript file to the template. 98 99 If the filename is absolute (i.e. starts with a slash), the generated link 100 will be based off the application root path. If it is relative, the link 101 will be based off the `/chrome/` path. 102 """ 103 scriptset = req.chrome .setdefault('scriptset', set ()) 104 if filename in scriptset: 105 return False # Already added that script 106 107 if filename .startswith('common/') and 'htdocs_location' in req.chrome : 108 href = Href (req.chrome ['htdocs_location']) 109 path = filename [7:] 110 else: 111 href = req.href 112 if not filename .startswith('/'): 113 href = href .chrome 114 path = filename 115 script = {'href': href (path ), 'type': mimetype} 116 117 req.chrome .setdefault('scripts', []).append(script) 118 scriptset.add(filename )
119
120 - def add_script_data (req, data):
121 """Add data to be made available in javascript scripts as global variables. 122 123 The keys in `data` provide the names of the global variables. The values 124 are converted to JSON and assigned to the corresponding variables. 125 """ 126 req.chrome .setdefault('script_data', {}).update (data )
127
128 - def add_javascript (req, filename):
129 """Deprecated: use `add_script()` instead.""" 130 add_script (req, filename , mimetype='text/javascript')
131
132 - def add_warning (req, msg, *args):
133 """Add a non-fatal warning to the request object. 134 When rendering pages, any warnings will be rendered to the user.""" 135 if args : 136 msg %= args 137 req.chrome ['warnings'].append(msg)
138
139 - def add_notice (req, msg, *args):
140 """Add an informational notice to the request object. 141 When rendering pages, any notice will be rendered to the user.""" 142 if args : 143 msg %= args 144 req.chrome ['notices'].append(msg)
145
146 - def add_ctxtnav (req, elm_or_label, href=None, title=None):
147 """Add an entry to the current page's ctxtnav bar.""" 148 if href : 149 elm = tag .a(elm_or_label, href =href , title =title ) 150 else: 151 elm = elm_or_label 152 req.chrome .setdefault('ctxtnav', []).append(elm)
153
154 - def prevnext_nav (req, prev_label, next_label, up_label=None):
155 """Add Previous/Up/Next navigation links. 156 157 @param req a `Request` object 158 @param prev_label the label to use for left (previous) link 159 @param up_label the label to use for the middle (up) link 160 @param next_label the label to use for right (next) link 161 """ 162 links = req.chrome ['links'] 163 prev_link = next_link = None 164 165 if not any(lnk in links for lnk in ('prev', 'up', 'next')): # Short circuit 166 return 167 168 if 'prev' in links: 169 prev = links['prev'][0] 170 prev_link = tag .a(prev_label, href =prev['href'], title =prev['title'], 171 class_='prev') 172 173 add_ctxtnav (req, tag .span(Markup('&larr; '), prev_link or prev_label, 174 class_=not prev_link and 'missing' or None)) 175 176 if up_label and 'up' in links: 177 up = links['up'][0] 178 add_ctxtnav (req, tag .a(up_label, href =up['href'], title =up['title'])) 179 180 if 'next' in links: 181 next_ = links['next'][0] 182 next_link = tag .a(next_label, href =next_['href'], title =next_['title'], 183 class_='next') 184 185 add_ctxtnav (req, tag .span(next_link or next_label, Markup(' &rarr;'), 186 class_=not next_link and 'missing' or None))
187 188
189 - def _save_messages (req, url, permanent):
190 """Save warnings and notices in case of redirect, so that they can 191 be displayed after the redirect.""" 192 for type_ in ['warnings', 'notices']: 193 for (i , message ) in enumerate(req.chrome [type_]): 194 req.session ['chrome.%s.%d' % (type_, i )] = escape (message )
195 196
197 - class INavigationContributor (Interface):
198 """Extension point interface for components that contribute items to the 199 navigation. 200 """ 201
202 - def get_active_navigation_item (req):
203 """This method is only called for the `IRequestHandler` processing the 204 request. 205 206 It should return the name of the navigation item that should be 207 highlighted as active/current. 208 """
209
210 - def get_navigation_items (req):
211 """Should return an iterable object over the list of navigation items to 212 add, each being a tuple in the form (category, name, text). 213 """
214 215
216 - class ITemplateProvider (Interface):
217 """Extension point interface for components that provide their own 218 ClearSilver templates and accompanying static resources. 219 """ 220
221 - def get_htdocs_dirs ():
222 """Return a list of directories with static resources (such as style 223 sheets, images, etc.) 224 225 Each item in the list must be a `(prefix, abspath)` tuple. The 226 `prefix` part defines the path in the URL that requests to these 227 resources are prefixed with. 228 229 The `abspath` is the absolute path to the directory containing the 230 resources on the local file system. 231 """
232
233 - def get_templates_dirs ():
234 """Return a list of directories containing the provided template 235 files. 236 """
237 238 239 # Mappings for removal of control characters 240 _translate_nop = "".join ([chr(i ) for i in range(256)]) 241 _invalid_control_chars = "".join ([chr(i ) for i in range(32) 242 if i not in [0x09, 0x0a, 0x0d]]) 243 244
245 - class Chrome (Component):
246 """Web site chrome assembly manager. 247 248 Chrome is everything that is not actual page content. 249 """ 250 required = True 251 252 implements (ISystemInfoProvider, IEnvironmentSetupParticipant , 253 IRequestHandler , ITemplateProvider , IWikiSyntaxProvider ) 254 255 navigation_contributors = ExtensionPoint (INavigationContributor ) 256 template_providers = ExtensionPoint (ITemplateProvider ) 257 stream_filters = ExtensionPoint (ITemplateStreamFilter ) 258 259 shared_templates_dir = PathOption ('inherit', 'templates_dir', '', 260 """Path to the //shared templates directory//. 261 262 Templates in that directory are loaded in addition to those in the 263 environments `templates` directory, but the latter take precedence. 264 265 (''since 0.11'')""") 266 267 auto_reload = BoolOption ('trac', 'auto_reload', False, 268 """Automatically reload template files after modification.""") 269 270 genshi_cache_size = IntOption ('trac', 'genshi_cache_size', 128, 271 """The maximum number of templates that the template loader will cache 272 in memory. The default value is 128. You may want to choose a higher 273 value if your site uses a larger number of templates, and you have 274 enough memory to spare, or you can reduce it if you are short on 275 memory.""") 276 277 htdocs_location = Option ('trac', 'htdocs_location', '', 278 """Base URL for serving the core static resources below 279 `/chrome/common/`. 280 281 It can be left empty, and Trac will simply serve those resources 282 itself. 283 284 Advanced users can use this together with 285 [TracAdmin trac-admin ... deploy <deploydir>] to allow serving the 286 static resources for Trac directly from the web server. 287 Note however that this only applies to the `<deploydir>/htdocs/common` 288 directory, the other deployed resources (i.e. those from plugins) 289 will not be made available this way and additional rewrite 290 rules will be needed in the web server.""") 291 292 metanav_order = ListOption ('trac', 'metanav', 293 'login,logout,prefs,help,about', doc= 294 """Order of the items to display in the `metanav` navigation bar, 295 listed by IDs. See also TracNavigation.""") 296 297 mainnav_order = ListOption ('trac', 'mainnav', 298 'wiki,timeline,roadmap,browser,tickets,' 299 'newticket,search', doc= 300 """Order of the items to display in the `mainnav` navigation bar, 301 listed by IDs. See also TracNavigation.""") 302 303 logo_link = Option ('header_logo', 'link', '', 304 """URL to link to, from the header logo.""") 305 306 logo_src = Option ('header_logo', 'src', 'site/your_project_logo.png', 307 """URL of the image to use as header logo. 308 It can be absolute, server relative or relative. 309 310 If relative, it is relative to one of the `/chrome` locations: 311 `site/your-logo.png` if `your-logo.png` is located in the `htdocs` 312 folder within your TracEnvironment; 313 `common/your-logo.png` if `your-logo.png` is located in the 314 folder mapped to the [#trac-section htdocs_location] URL. 315 Only specifying `your-logo.png` is equivalent to the latter.""") 316 317 logo_alt = Option ('header_logo', 'alt', 318 "(please configure the [header_logo] section in trac.ini)", 319 """Alternative text for the header logo.""") 320 321 logo_width = IntOption ('header_logo', 'width', -1, 322 """Width of the header logo image in pixels.""") 323 324 logo_height = IntOption ('header_logo', 'height', -1, 325 """Height of the header logo image in pixels.""") 326 327 show_email_addresses = BoolOption ('trac', 'show_email_addresses', 'false', 328 """Show email addresses instead of usernames. If false, we obfuscate 329 email addresses. (''since 0.11'')""") 330 331 never_obfuscate_mailto = BoolOption ('trac', 'never_obfuscate_mailto', 332 'false', 333 """Never obfuscate `mailto:` links explicitly written in the wiki, 334 even if `show_email_addresses` is false or the user has not the 335 EMAIL_VIEW permission (''since 0.11.6'').""") 336 337 show_ip_addresses = BoolOption ('trac', 'show_ip_addresses', 'false', 338 """Show IP addresses for resource edits (e.g. wiki). 339 (''since 0.11.3'')""") 340 341 resizable_textareas = BoolOption ('trac', 'resizable_textareas', 'true', 342 """Make `<textarea>` fields resizable. Requires !JavaScript. 343 (''since 0.12'')""") 344 345 auto_preview_timeout = FloatOption ('trac', 'auto_preview_timeout', 2.0, 346 """Inactivity timeout in seconds after which the automatic wiki preview 347 triggers an update. This option can contain floating-point values. The 348 lower the setting, the more requests will be made to the server. Set 349 this to 0 to disable automatic preview. The default is 2.0 seconds. 350 (''since 0.12'')""") 351 352 templates = None 353 354 # A dictionary of default context data for templates 355 _default_context_data = { 356 '_': translation .gettext, 357 'all': compat .all, 358 'any': compat .any, 359 'classes': presentation .classes , 360 'date': datetime.date, 361 'datetime': datetime.datetime, 362 'dgettext': translation .dgettext, 363 'dngettext': translation .dngettext, 364 'first_last': presentation .first_last , 365 'get_reporter_id': get_reporter_id , 366 'gettext': translation .gettext, 367 'group': presentation .group , 368 'groupby': compat .py_groupby , # http://bugs.python.org/issue2246 369 'http_date': http_date , 370 'istext': presentation .istext , 371 'javascript_quote': javascript_quote , 372 'ngettext': translation .ngettext , 373 'paginate': presentation .paginate , 374 'partial': partial, 375 'pathjoin': pathjoin , 376 'plaintext': plaintext , 377 'pprint': pprint.pformat, 378 'pretty_size': pretty_size , 379 'pretty_timedelta': pretty_timedelta , 380 'quote_plus': unicode_quote_plus , 381 'reversed': reversed, 382 'separated': presentation .separated, 383 'shorten_line': shorten_line , 384 'sorted': sorted, 385 'time': datetime.time, 386 'timedelta': datetime.timedelta, 387 'to_json': presentation .to_json, 388 'to_unicode': to_unicode , 389 'utc': utc , 390 } 391 392 # ISystemInfoProvider methods 393
394 - def get_system_info (self):
395 import genshi 396 info = get_pkginfo (genshi).get ('version') 397 if hasattr(genshi, '_speedups'): 398 info += ' (with speedups)' 399 else: 400 info += ' (without speedups)' 401 yield 'Genshi', info 402 try: 403 import babel 404 except ImportError: 405 babel = None 406 if babel is not None: 407 info = get_pkginfo (babel).get ('version') 408 if not get_available_locales(): 409 info += " (translations unavailable)" # No i18n on purpose 410 self.log .warning("Locale data is missing") 411 yield 'Babel', info
412 413 # IEnvironmentSetupParticipant methods 414
415 - def environment_created (self):
416 """Create the environment templates directory.""" 417 if self.env .path : 418 templates_dir = os.path .join (self.env .path , 'templates') 419 if not os.path .exists (templates_dir): 420 os.mkdir(templates_dir) 421 422 site_path = os.path .join (templates_dir, 'site.html.sample') 423 fileobj = open (site_path, 'w') 424 try: 425 fileobj.write ("""\ 426 <html xmlns="http://www.w3.org/1999/xhtml" 427 xmlns:xi="http://www.w3.org/2001/XInclude" 428 xmlns:py="http://genshi.edgewall.org/" 429 py:strip=""> 430 <!--! 431 This file allows customizing the appearance of the Trac installation. 432 Add your customizations here and rename the file to site.html. Note that 433 it will take precedence over a global site.html placed in the directory 434 specified by [inherit] templates_dir. 435 436 More information about site appearance customization can be found here: 437 438 http://trac.edgewall.org/wiki/TracInterfaceCustomization#SiteAppearance 439 --> 440 </html> 441 """) 442 finally: 443 fileobj.close ()
444
445 - def environment_needs_upgrade (self, db):
446 return False
447
448 - def upgrade_environment (self, db):
449 pass
450 451 # IRequestHandler methods 452
453 - def match_request (self, req):
454 match = re.match(r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>.+)', 455 req.path_info ) 456 if match: 457 req.args ['prefix'] = match.group ('prefix') 458 req.args ['filename'] = match.group ('filename') 459 return True
460
461 - def process_request (self, req):
462 prefix = req.args ['prefix'] 463 filename = req.args ['filename'] 464 465 dirs = [] 466 for provider in self.template_providers : 467 for dir in [os.path .normpath(dir[1]) for dir 468 in provider.get_htdocs_dirs () or [] 469 if dir[0] == prefix]: 470 dirs .append(dir) 471 path = os.path .normpath(os.path .join (dir, filename )) 472 if os.path .commonprefix([dir, path ]) != dir: 473 raise TracError (_ ("Invalid chrome path %(path)s.", 474 path =filename )) 475 elif os.path .isfile (path ): 476 req.send_file (path , get_mimetype (path )) 477 478 self.log .warning('File %s not found in any of %s', filename , dirs ) 479 raise HTTPNotFound ('File %s not found', filename )
480 481 # ITemplateProvider methods 482
483 - def get_htdocs_dirs (self):
484 return [('common', pkg_resources.resource_filename('trac', 'htdocs')), 485 ('site', self.env .get_htdocs_dir ())]
486
487 - def get_templates_dirs (self):
488 return filter(None, [ 489 self.env .get_templates_dir (), 490 self.shared_templates_dir , 491 pkg_resources.resource_filename('trac', 'templates'), 492 ])
493 494 # IWikiSyntaxProvider methods 495
496 - def get_wiki_syntax (self):
497 return []
498 501 506 507 # Public API methods 508
509 - def get_all_templates_dirs (self):
510 """Return a list of the names of all known templates directories.""" 511 dirs = [] 512 for provider in self.template_providers : 513 dirs .extend(provider.get_templates_dirs () or []) 514 return dirs
515
516 - def prepare_request (self, req, handler=None):
517 """Prepare the basic chrome data for the request. 518 519 @param req: the request object 520 @param handler: the `IRequestHandler` instance that is processing the 521 request 522 """ 523 self.log .debug('Prepare chrome data for request') 524 525 chrome = {'metas': [], 'links': {}, 'scripts': [], 'script_data': {}, 526 'ctxtnav': [], 'warnings': [], 'notices': []} 527 setattr(req, 'chrome', chrome ) 528 529 htdocs_location = self.htdocs_location or req.href .chrome ('common') 530 chrome ['htdocs_location'] = htdocs_location .rstrip('/') + '/' 531 532 # HTML <head> links 533 add_link (req, 'start', req.href .wiki ()) 534 add_link (req, 'search', req.href .search ()) 535 add_link (req, 'help', req.href .wiki ('TracGuide')) 536 add_stylesheet (req, 'common/css/trac.css') 537 add_script (req, 'common/js/jquery.js') 538 # Only activate noConflict mode if requested to by the handler 539 if handler is not None and \ 540 getattr(handler .__class__, 'jquery_noconflict', False): 541 add_script (req, 'common/js/noconflict.js') 542 add_script (req, 'common/js/babel.js') 543 if req.locale is not None: 544 add_script (req, 'common/js/messages/%s.js' % req.locale ) 545 add_script (req, 'common/js/trac.js') 546 add_script (req, 'common/js/search.js') 547 548 # Shortcut icon 549 chrome ['icon'] = self.get_icon_data (req) 550 if chrome ['icon']: 551 src = chrome ['icon']['src'] 552 mimetype = chrome ['icon']['mimetype'] 553 add_link (req, 'icon', src, mimetype=mimetype) 554 add_link (req, 'shortcut icon', src, mimetype=mimetype) 555 556 # Logo image 557 chrome ['logo'] = self.get_logo_data (req.href , req.abs_href ) 558 559 # Navigation links 560 allitems = {} 561 active = None 562 for contributor in self.navigation_contributors : 563 try: 564 for category, name , text in \ 565 contributor.get_navigation_items (req) or []: 566 category_section = self.config [category] 567 if category_section.getbool (name , True): 568 # the navigation item is enabled (this is the default) 569 item = None 570 if isinstance(text , Element) and \ 571 text .tag .localname == 'a': 572 item = text 573 label = category_section.get (name + '.label') 574 href = category_section.get (name + '.href') 575 if href : 576 if href .startswith('/'): 577 href = req.href + href 578 if label: 579 item = tag .a(label) # create new label 580 elif not item: 581 item = tag .a(text ) # wrap old text 582 item = item(href =href ) # use new href 583 elif label and item: # create new label, use old href 584 item = tag .a(label, href =item.attrib.get ('href')) 585 elif not item: # use old text 586 item = text 587 allitems.setdefault(category, {})[name ] = item 588 if contributor is handler : 589 active = contributor.get_active_navigation_item (req) 590 except Exception, e : 591 name = contributor.__class__.__name__ 592 if isinstance(e , TracError ): 593 self.log .warning("Error with navigation contributor %s", 594 name ) 595 else: 596 self.log .error ("Error with navigation contributor %s: %s", 597 name , exception_to_unicode (e )) 598 add_warning (req, _ ("Error with navigation contributor " 599 '"%(name)s"', name =name )) 600 601 nav = {} 602 for category, items in [(k, v.items()) for k, v in allitems.items()]: 603 category_order = category + '_order' 604 if hasattr(self, category_order): 605 order = getattr(self, category_order) 606 def navcmp(x, y): 607 if x [0] not in order: 608 return int(y[0] in order) 609 if y[0] not in order: 610 return -int(x [0] in order) 611 return cmp(order.index(x [0]), order.index(y[0]))
612 items.sort(navcmp) 613 614 nav[category] = [] 615 for name , label in items: 616 nav[category].append({ 617 'name': name , 618 'label': label, 619 'active': name == active 620 }) 621 622 chrome ['nav'] = nav 623 624 # Default theme file 625 chrome ['theme'] = 'theme.html' 626 627 # Avoid recursion by registering as late as possible (#8583) 628 req.add_redirect_listener (_save_messages) 629 630 return chrome
631
632 - def get_icon_data (self, req):
633 icon = {} 634 icon_src = icon_abs_src = self.env .project_icon 635 if icon_src: 636 if not icon_src.startswith('/') and icon_src.find('://') == -1: 637 if '/' in icon_src: 638 icon_abs_src = req.abs_href .chrome (icon_src) 639 icon_src = req.href .chrome (icon_src) 640 else: 641 icon_abs_src = req.abs_href .chrome ('common', icon_src) 642 icon_src = req.href .chrome ('common', icon_src) 643 mimetype = get_mimetype (icon_src) 644 icon = {'src': icon_src, 'abs_src': icon_abs_src, 645 'mimetype': mimetype} 646 return icon
647
648 - def get_logo_data (self, href, abs_href=None):
649 # TODO: Possibly, links to 'common/' could use chrome.htdocs_location 650 logo = {} 651 logo_src = self.logo_src 652 if logo_src : 653 abs_href = abs_href or href 654 if logo_src .startswith('http://') or \ 655 logo_src .startswith('https://') or \ 656 logo_src .startswith('/'): 657 # Nothing further can be calculated 658 logo_src_abs = logo_src 659 elif '/' in logo_src : 660 # Like 'common/trac_banner.png' or 'site/my_banner.png' 661 logo_src_abs = abs_href .chrome (logo_src ) 662 logo_src = href .chrome (logo_src ) 663 else: 664 # Like 'trac_banner.png' 665 logo_src_abs = abs_href .chrome ('common', logo_src ) 666 logo_src = href .chrome ('common', logo_src ) 667 width = self.logo_width > -1 and self.logo_width or None 668 height = self.logo_height > -1 and self.logo_height or None 669 logo = { 670 'link': self.logo_link , 'src': logo_src , 671 'src_abs': logo_src_abs, 'alt': self.logo_alt , 672 'width': width, 'height': height 673 } 674 else: 675 logo = {'link': self.logo_link , 'alt': self.logo_alt } 676 return logo
677
678 - def populate_hdf (self, req):
679 """Add chrome-related data to the HDF (deprecated).""" 680 req.hdf['HTTP.PathInfo'] = req.path_info 681 req.hdf['htdocs_location'] = req.chrome ['htdocs_location'] 682 683 req.hdf['chrome.href'] = req.href .chrome () 684 req.hdf['chrome.links'] = req.chrome ['links'] 685 req.hdf['chrome.scripts'] = req.chrome ['scripts'] 686 req.hdf['chrome.logo'] = req.chrome ['logo'] 687 688 for category, items in req.chrome ['nav'].items(): 689 req.hdf['chrome.nav.%s' % category] = items
690
691 - def populate_data (self, req, data):
692 d = self._default_context_data .copy() 693 d ['trac'] = { 694 'version': VERSION, 695 'homepage': 'http://trac.edgewall.org/', # FIXME: use setup data 696 } 697 698 href = req and req.href 699 abs_href = req and req.abs_href or self.env .abs_href 700 admin_href = None 701 if self.env .project_admin_trac_url == '.': 702 admin_href = href 703 elif self.env .project_admin_trac_url : 704 admin_href = Href (self.env .project_admin_trac_url ) 705 706 d ['project'] = { 707 'name': self.env .project_name , 708 'descr': self.env .project_description , 709 'url': self.env .project_url , 710 'admin': self.env .project_admin , 711 'admin_href': admin_href, 712 'admin_trac_url': self.env .project_admin_trac_url , 713 } 714 footer = self.env .project_footer 715 d ['chrome'] = { 716 'footer': Markup(footer and translation .gettext(footer)) 717 } 718 if req: 719 d ['chrome'].update (req.chrome ) 720 else: 721 d ['chrome'].update ({ 722 'htdocs_location': self.htdocs_location , 723 'logo': self.get_logo_data (self.env .abs_href ), 724 }) 725 726 try: 727 show_email_addresses = (self.show_email_addresses or not req or \ 728 'EMAIL_VIEW' in req.perm ) 729 except Exception, e : 730 # simply log the exception here, as we might already be rendering 731 # the error page 732 self.log .error ("Error during check of EMAIL_VIEW: %s", 733 exception_to_unicode (e )) 734 show_email_addresses = False 735 tzinfo = None 736 if req: 737 tzinfo = req.tz 738 739 def dateinfo(date): 740 return tag .span(pretty_timedelta (date), 741 title =format_datetime (date))
742 743 def get_rel_url(resource, **kwargs): 744 return get_resource_url (self.env , resource , href , **kwargs)
745 746 def get_abs_url(resource, **kwargs): 747 return get_resource_url (self.env , resource , abs_href , **kwargs) 748 749 d .update ({ 750 'context': req and Context .from_request (req) or None, 751 'Resource': Resource , 752 'url_of': get_rel_url, 753 'abs_url_of': get_abs_url, 754 'name_of': partial(get_resource_name , self.env ), 755 'shortname_of': partial(get_resource_shortname , self.env ), 756 'summary_of': partial(get_resource_summary , self.env ), 757 'req': req, 758 'abs_href': abs_href , 759 'href': href , 760 'perm': req and req.perm , 761 'authname': req and req.authname or '<trac>', 762 'locale': req and req.locale , 763 'show_email_addresses': show_email_addresses , 764 'show_ip_addresses': self.show_ip_addresses , 765 'authorinfo': partial(self.authorinfo , req), 766 'authorinfo_short': self.authorinfo_short , 767 'format_author': partial(self.format_author , req), 768 'format_emails': self.format_emails , 769 'get_systeminfo': self.env .get_systeminfo , 770 771 # Date/time formatting 772 'dateinfo': dateinfo, 773 'format_datetime': partial(format_datetime , tzinfo=tzinfo), 774 'format_date': partial(format_date , tzinfo=tzinfo), 775 'format_time': partial(format_time , tzinfo=tzinfo), 776 'fromtimestamp': partial(datetime.datetime.fromtimestamp, 777 tz =tzinfo), 778 'from_utimestamp': from_utimestamp , 779 780 # Wiki-formatting functions 781 'wiki_to': partial(format_to, self.env ), 782 'wiki_to_html': partial(format_to_html, self.env ), 783 'wiki_to_oneliner': partial(format_to_oneliner, self.env ), 784 }) 785 786 # Finally merge in the page-specific data 787 d .update (data ) 788 return d 789
790 - def load_template (self, filename, method=None):
791 """Retrieve a Template and optionally preset the template data. 792 793 Also, if the optional `method` argument is set to `'text'`, a 794 `NewTextTemplate` instance will be created instead of a 795 `MarkupTemplate`. 796 """ 797 if not self.templates : 798 self.templates = TemplateLoader( 799 self.get_all_templates_dirs (), auto_reload =self.auto_reload , 800 max_cache_size=self.genshi_cache_size , 801 variable_lookup='lenient', callback=lambda template: 802 Translator(translation .get_translations()).setup(template)) 803 804 if method == 'text': 805 cls = NewTextTemplate 806 else: 807 cls = MarkupTemplate 808 809 return self.templates .load (filename , cls=cls)
810
811 - def render_template (self, req, filename, data, content_type=None, 812 fragment=False):
813 """Render the `filename` using the `data` for the context. 814 815 The `content_type` argument is used to choose the kind of template 816 used (`NewTextTemplate` if `'text/plain'`, `MarkupTemplate` otherwise), 817 and tweak the rendering process (use of XHTML Strict doctype if 818 `'text/html'` is given). 819 820 When `fragment` is specified, the (filtered) Genshi stream is 821 returned. 822 """ 823 if content_type is None: 824 content_type = 'text/html' 825 method = {'text/html': 'xhtml', 826 'text/plain': 'text'}.get (content_type , 'xml') 827 828 if method == "xhtml": 829 # Retrieve post-redirect messages saved in session 830 for type_ in ['warnings', 'notices']: 831 try: 832 for i in itertools.count (): 833 message = req.session .pop('chrome.%s.%d' % (type_, i )) 834 req.chrome [type_].append(Markup(message )) 835 except KeyError: 836 pass 837 838 template = self.load_template (filename , method =method ) 839 data = self.populate_data (req, data ) 840 data ['chrome']['content_type'] = content_type 841 842 stream = template.generate(**data ) 843 844 # Filter through ITemplateStreamFilter plugins 845 if self.stream_filters : 846 stream |= self._filter_stream(req, method , filename , stream, data ) 847 848 if fragment: 849 return stream 850 851 if method == 'text': 852 buffer = StringIO() 853 stream.render ('text', out=buffer, encoding='utf-8') 854 return buffer.getvalue() 855 856 doctype = {'text/html': DocType.XHTML_STRICT}.get (content_type ) 857 if doctype: 858 if req.form_token: 859 stream |= self._add_form_token(req.form_token) 860 if not int(req.session .get ('accesskeys', 0)): 861 stream |= self._strip_accesskeys 862 863 links = req.chrome .get ('links') 864 scripts = req.chrome .get ('scripts') 865 script_data = req.chrome .get ('script_data') 866 req.chrome ['links'] = {} 867 req.chrome ['scripts'] = [] 868 req.chrome ['script_data'] = {} 869 data .setdefault('chrome', {}).update ({ 870 'late_links': req.chrome ['links'], 871 'late_scripts': req.chrome ['scripts'], 872 'late_script_data': req.chrome ['script_data'], 873 }) 874 875 try: 876 buffer = StringIO() 877 stream.render (method , doctype=doctype, out=buffer, 878 encoding='utf-8') 879 return buffer.getvalue().translate(_translate_nop , 880 _invalid_control_chars ) 881 except Exception, e : 882 # restore what may be needed by the error template 883 req.chrome ['links'] = links 884 req.chrome ['scripts'] = scripts 885 req.chrome ['script_data'] = script_data 886 # give some hints when hitting a Genshi unicode error 887 if isinstance(e , UnicodeError): 888 pos = self._stream_location(stream) 889 if pos: 890 location = "'%s', line %s, char %s" % pos 891 else: 892 location = _ ("(unknown template location)") 893 raise TracError (_ ("Genshi %(error)s error while rendering " 894 "template %(location)s", 895 error =e .__class__.__name__, 896 location=location)) 897 raise
898 899 # E-mail formatting utilities 900
901 - def cc_list (self, cc_field):
902 """Split a CC: value in a list of addresses.""" 903 ccs = [] 904 for cc in re.split(r'[;,]', cc_field): 905 cc = cc.strip() 906 if cc: 907 ccs.append(cc) 908 return ccs
909
910 - def format_emails (self, context, value, sep=', '):
911 """Normalize a list of e-mails and obfuscate them if needed. 912 913 :param context: the context in which the check for obfuscation should 914 be done 915 :param value: a string containing a comma-separated list of e-mails 916 :param sep: the separator to use when rendering the list again 917 """ 918 all_cc = self.cc_list (value) 919 if not (self.show_email_addresses or 'EMAIL_VIEW' in context.perm ): 920 all_cc = [obfuscate_email_address (cc) for cc in all_cc] 921 return sep.join (all_cc)
922
923 - def authorinfo (self, req, author, email_map=None):
924 return self.format_author (req, 925 email_map and '@' not in author and 926 email_map.get (author) or author)
927
928 - def get_email_map (self):
929 """Get the email addresses of all known users.""" 930 email_map = {} 931 if self.show_email_addresses : 932 for username , name , email in self.env .get_known_users (): 933 if email: 934 email_map[username ] = email 935 return email_map
936 937 _long_author_re = re.compile(r'.*<([^@]+)@[^@]+>\s*|([^@]+)@[^@]+') 938
939 - def authorinfo_short (self, author):
940 if not author or author == 'anonymous': 941 return _ ("anonymous") 942 match = self._long_author_re .match(author) 943 if match: 944 return match.group (1) or match.group (2) 945 return author
946
947 - def format_author (self, req, author):
948 if not author or author == 'anonymous': 949 return _ ("anonymous") 950 if self.show_email_addresses or not req or 'EMAIL_VIEW' in req.perm : 951 return author 952 return obfuscate_email_address (author)
953 954 # Element modifiers 955
956 - def add_textarea_grips (self, req):
957 """Make `<textarea class="trac-resizable">` fields resizable if enabled 958 by configuration.""" 959 if self.resizable_textareas : 960 add_script (req, 'common/js/resizer.js')
961
962 - def add_wiki_toolbars (self, req):
963 """Add wiki toolbars to `<textarea class="wikitext">` fields.""" 964 add_script (req, 'common/js/wikitoolbar.js') 965 self.add_textarea_grips (req)
966
967 - def add_auto_preview (self, req):
968 """Setup auto-preview for `<textarea>` fields.""" 969 add_script (req, 'common/js/auto_preview.js') 970 add_script_data (req, { 971 'auto_preview_timeout': self.auto_preview_timeout , 972 'form_token': req.form_token})
973 974 # Template filters 975
976 - def _add_form_token (self, token):
977 elem = tag .div( 978 tag .input(type ='hidden', name ='__FORM_TOKEN', value=token) 979 ) 980 def _generate(stream, ctxt=None): 981 for kind, data , pos in stream: 982 if kind is START and data [0].localname == 'form' \ 983 and data [1].get ('method', '').lower() == 'post': 984 yield kind, data , pos 985 for event in elem.generate(): 986 yield event 987 else: 988 yield kind, data , pos
989 return _generate 990
991 - def _strip_accesskeys (self, stream, ctxt=None):
992 for kind, data , pos in stream: 993 if kind is START and 'accesskey' in data [1]: 994 data = data [0], Attrs([(k, v) for k, v in data [1] 995 if k != 'accesskey']) 996 yield kind, data , pos
997
998 - def _filter_stream (self, req, method, filename, stream, data):
999 def inner(stream, ctxt=None): 1000 for filter in self.stream_filters : 1001 stream = filter.filter_stream (req, method , filename , stream, 1002 data ) 1003 return stream
1004 return inner 1005
1006 - def _stream_location (self, stream):
1007 for kind, data , pos in stream: 1008 return pos
1009
Trees Indices Help
Trac
Generated by Epydoc 3.0.1 on Mon Feb 13 23:37:31 2023 http://epydoc.sourceforge.net

AltStyle によって変換されたページ (->オリジナル) /