Package trac :: Package web :: Module chrome

Source Code for Module trac.web.chrome

   1  # -*- coding: utf-8 -*- 
   2  # 
   3  # Copyright (C) 2005-2020 Edgewall Software 
   4  # Copyright (C) 2005-2006 Christopher Lenz <[email protected]> 
   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 https://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 https://trac.edgewall.org/log/. 
  14  # 
  15  # Author: Christopher Lenz <[email protected]> 
  16   
  17  """Content presentation for the web layer. 
  18   
  19  The Chrome module deals with delivering and shaping content to the end user, 
  20  mostly targeting (X)HTML generation but not exclusively, RSS or other forms of 
  21  web content are also using facilities provided here. 
  22  """ 
  23   
  24  import datetime 
  25  from functools import partial 
  26  import itertools 
  27  import operator 
  28  import os.path 
  29  import pkg_resources 
  30  import pprint 
  31  import re 
  32  try: 
  33      from cStringIO import StringIO 
  34  except ImportError: 
  35      from StringIO import StringIO 
  36   
  37  from genshi import Markup 
  38  from genshi.builder import tag, Element 
  39  from genshi.core import Attrs, START 
  40  from genshi.filters import Translator 
  41  from genshi.output import DocType 
  42  from genshi.template import TemplateLoader, MarkupTemplate, NewTextTemplate 
  43   
  44  from trac.config import * 
  45  from trac.core import * 
  46  from trac.env import IEnvironmentSetupParticipant, ISystemInfoProvider 
  47  from trac.mimeview.api import RenderingContext, get_mimetype 
  48  from trac.perm import IPermissionRequestor 
  49  from trac.resource import * 
  50  from trac.util import as_bool, as_int, compat, get_reporter_id, html,\ 
  51                        presentation, get_pkginfo, pathjoin, translation 
  52  from trac.util.html import escape, plaintext 
  53  from trac.util.text import ( 
  54      exception_to_unicode, is_obfuscated, javascript_quote, 
  55      obfuscate_email_address, pretty_size, shorten_line, to_js_string, 
  56      to_unicode, unicode_quote_plus) 
  57  from trac.util.datefmt import ( 
  58      pretty_timedelta, datetime_now, format_datetime, format_date, format_time, 
  59      from_utimestamp, http_date, utc, get_date_format_jquery_ui, is_24_hours, 
  60      get_time_format_jquery_ui, user_time, get_month_names_jquery_ui, 
  61      get_day_names_jquery_ui, get_timezone_list_jquery_ui, 
  62      get_first_week_day_jquery_ui, get_timepicker_separator_jquery_ui, 
  63      get_period_names_jquery_ui, localtz) 
  64  from trac.util.html import to_fragment 
  65  from trac.util.translation import _, get_available_locales 
  66  from trac.web.api import IRequestHandler, ITemplateStreamFilter, HTTPNotFound 
  67  from trac.web.href import Href 
  68  from trac.wiki import IWikiSyntaxProvider 
  69  from trac.wiki.formatter import format_to, format_to_html, format_to_oneliner 
  70   
  71  default_mainnav_order = ('wiki', 'timeline', 'roadmap', 'browser', 
  72                           'tickets', 'newticket', 'search', 'admin') 
  73  default_metanav_order = ('login', 'logout', 'prefs', 'help', 'about') 
  74   
  75   
76 -class INavigationContributor(Interface):
77 """Extension point interface for components that contribute items to the 78 navigation. 79 """ 80
82 """This method is only called for the `IRequestHandler` processing the 83 request. 84 85 It should return the name of the navigation item that should be 86 highlighted as active/current. 87 """
88
89 - def get_navigation_items(req):
90 """Should return an iterable object over the list of navigation items 91 to add, each being a tuple in the form (category, name, text). 92 """
93 94
95 -class ITemplateProvider(Interface):
96 """Extension point interface for components that provide their own 97 Genshi templates and accompanying static resources. 98 """ 99
100 - def get_htdocs_dirs():
101 """Return a list of directories with static resources (such as style 102 sheets, images, etc.) 103 104 Each item in the list must be a `(prefix, abspath)` tuple. The 105 `prefix` part defines the path in the URL that requests to these 106 resources are prefixed with. 107 108 The `abspath` is the absolute path to the directory containing the 109 resources on the local file system. 110 """
111
112 - def get_templates_dirs():
113 """Return a list of directories containing the provided template 114 files. 115 """
116 117
118 -def add_meta(req, content, http_equiv=None, name=None, scheme=None, lang=None):
119 """Add a `<meta>` tag into the `<head>` of the generated HTML.""" 120 meta = {'content': content, 'http-equiv': http_equiv, 'name': name, 121 'scheme': scheme, 'lang': lang, 'xml:lang': lang} 122 req.chrome.setdefault('metas', []).append(meta)
123 124 140 141
142 -def add_stylesheet(req, filename, mimetype='text/css', **attrs):
143 """Add a link to a style sheet to the chrome info so that it gets included 144 in the generated HTML page. 145 146 If `filename` is a network-path reference (i.e. starts with a protocol 147 or `//`), the return value will not be modified. If `filename` is absolute 148 (i.e. starts with `/`), the generated link will be based off the 149 application root path. If it is relative, the link will be based off the 150 `/chrome/` path. 151 """ 152 href = chrome_resource_path(req, filename) 153 add_link(req, 'stylesheet', href, mimetype=mimetype, **attrs)
154 155
156 -def add_script(req, filename, mimetype='text/javascript', charset='utf-8', 157 ie_if=None):
158 """Add a reference to an external javascript file to the template. 159 160 If `filename` is a network-path reference (i.e. starts with a protocol 161 or `//`), the return value will not be modified. If `filename` is absolute 162 (i.e. starts with `/`), the generated link will be based off the 163 application root path. If it is relative, the link will be based off the 164 `/chrome/` path. 165 """ 166 scriptset = req.chrome.setdefault('scriptset', set()) 167 if filename in scriptset: 168 return False # Already added that script 169 170 href = chrome_resource_path(req, filename) 171 script = {'href': href, 'type': mimetype, 'charset': charset, 172 'prefix': Markup('<!--[if %s]>' % ie_if) if ie_if else None, 173 'suffix': Markup('<![endif]-->') if ie_if else None} 174 175 req.chrome.setdefault('scripts', []).append(script) 176 scriptset.add(filename)
177 178
179 -def add_script_data(req, data={}, **kwargs):
180 """Add data to be made available in javascript scripts as global variables. 181 182 The keys in `data` and the keyword argument names provide the names of the 183 global variables. The values are converted to JSON and assigned to the 184 corresponding variables. 185 """ 186 script_data = req.chrome.setdefault('script_data', {}) 187 script_data.update(data) 188 script_data.update(kwargs)
189 190
191 -def add_warning(req, msg, *args):
192 """Add a non-fatal warning to the request object. 193 194 When rendering pages, all warnings will be rendered to the user. Note that 195 the message is escaped (and therefore converted to `Markup`) before it is 196 stored in the request object. 197 """ 198 _add_message(req, 'warnings', msg, args)
199 200
201 -def add_notice(req, msg, *args):
202 """Add an informational notice to the request object. 203 204 When rendering pages, all notices will be rendered to the user. Note that 205 the message is escaped (and therefore converted to `Markup`) before it is 206 stored in the request object. 207 """ 208 _add_message(req, 'notices', msg, args)
209 210
211 -def _add_message(req, name, msg, args):
212 if args: 213 msg %= args 214 if not isinstance(msg, Markup): 215 msg = Markup(to_fragment(msg)) 216 if msg not in req.chrome[name]: 217 req.chrome[name].append(msg)
218 219
220 -def add_ctxtnav(req, elm_or_label, href=None, title=None):
221 """Add an entry to the current page's ctxtnav bar.""" 222 if href: 223 elm = tag.a(elm_or_label, href=href, title=title) 224 else: 225 elm = elm_or_label 226 req.chrome.setdefault('ctxtnav', []).append(elm)
227 228
229 -def prevnext_nav(req, prev_label, next_label, up_label=None):
230 """Add Previous/Up/Next navigation links. 231 232 :param req: a `Request` object 233 :param prev_label: the label to use for left (previous) link 234 :param up_label: the label to use for the middle (up) link 235 :param next_label: the label to use for right (next) link 236 """ 237 links = req.chrome['links'] 238 prev_link = next_link = None 239 240 if not any(lnk in links for lnk in ('prev', 'up', 'next')): # Short circuit 241 return 242 243 if 'prev' in links: 244 prev = links['prev'][0] 245 prev_link = tag.a(prev_label, href=prev['href'], title=prev['title'], 246 class_='prev') 247 248 add_ctxtnav(req, tag.span(Markup('&larr; '), prev_link or prev_label, 249 class_='missing' if not prev_link else None)) 250 251 if up_label and 'up' in links: 252 up = links['up'][0] 253 add_ctxtnav(req, tag.a(up_label, href=up['href'], title=up['title'])) 254 255 if 'next' in links: 256 next_ = links['next'][0] 257 next_link = tag.a(next_label, href=next_['href'], title=next_['title'], 258 class_='next') 259 260 add_ctxtnav(req, tag.span(next_link or next_label, Markup(' &rarr;'), 261 class_='missing' if not next_link else None))
262 263
264 -def web_context(req, resource=None, id=False, version=False, parent=False, 265 absurls=False):
266 """Create a rendering context from a request. 267 268 The `perm` and `href` properties of the context will be initialized 269 from the corresponding properties of the request object. 270 271 >>> from trac.test import Mock, MockPerm 272 >>> req = Mock(href=Mock(), perm=MockPerm()) 273 >>> context = web_context(req) 274 >>> context.href is req.href 275 True 276 >>> context.perm is req.perm 277 True 278 279 :param req: the HTTP request object 280 :param resource: the `Resource` object or realm 281 :param id: the resource identifier 282 :param version: the resource version 283 :param absurls: whether URLs generated by the ``href`` object should 284 be absolute (including the protocol scheme and host 285 name) 286 :return: a new rendering context 287 :rtype: `RenderingContext` 288 289 :since: version 1.0 290 """ 291 if req: 292 href = req.abs_href if absurls else req.href 293 perm = req.perm 294 else: 295 href = None 296 perm = None 297 self = RenderingContext(Resource(resource, id=id, version=version, 298 parent=parent), href=href, perm=perm) 299 self.req = req 300 return self
301 302 313 314
315 -def chrome_info_script(req, use_late=None):
316 """Get script elements from chrome info of the request object during 317 rendering template or after rendering. 318 319 :param req: the HTTP request object. 320 :param use_late: if True, `late_links` will be used instead of `links`. 321 """ 322 chrome = req.chrome 323 if use_late: 324 links = chrome.get('late_links', {}).get('stylesheet', []) 325 scripts = chrome.get('late_scripts', []) 326 script_data = chrome.get('late_script_data', {}) 327 else: 328 links = chrome.get('early_links', {}).get('stylesheet', []) + \ 329 chrome.get('links', {}).get('stylesheet', []) 330 scripts = chrome.get('early_scripts', []) + chrome.get('scripts', []) 331 script_data = {} 332 script_data.update(chrome.get('early_script_data', {})) 333 script_data.update(chrome.get('script_data', {})) 334 335 content = [] 336 content.extend('jQuery.loadStyleSheet(%s, %s);' % 337 (to_js_string(link['href']), to_js_string(link['type'])) 338 for link in links or ()) 339 content.extend('var %s=%s;' % (name, presentation.to_json(value)) 340 for name, value in (script_data or {}).iteritems()) 341 342 fragment = tag() 343 if content: 344 fragment.append(tag.script('\n'.join(content), type='text/javascript')) 345 for script in scripts: 346 fragment.append(script['prefix']) 347 fragment.append(tag.script( 348 'jQuery.loadScript(%s, %s, %s)' % 349 (to_js_string(script['href']), to_js_string(script['type']), 350 to_js_string(script['charset'])), type='text/javascript')) 351 fragment.append(script['suffix']) 352 353 return fragment
354 355
356 -def chrome_resource_path(req, filename):
357 """Get the path for a chrome resource given its `filename`. 358 359 If `filename` is a network-path reference (i.e. starts with a protocol 360 or `//`), the return value will not be modified. If `filename` is absolute 361 (i.e. starts with `/`), the generated link will be based off the 362 application root path. If it is relative, the link will be based off the 363 `/chrome/` path. 364 """ 365 if filename.startswith(('http://', 'https://', '//')): 366 return filename 367 elif filename.startswith('common/') and 'htdocs_location' in req.chrome: 368 return Href(req.chrome['htdocs_location'])(filename[7:]) 369 else: 370 href = req.href if filename.startswith('/') else req.href.chrome 371 return href(filename)
372 373 374 _chrome_resource_path = chrome_resource_path # will be removed in 1.3.1 375 376
377 -def _save_messages(req, url, permanent):
378 """Save warnings and notices in case of redirect, so that they can 379 be displayed after the redirect.""" 380 for type_ in ['warnings', 'notices']: 381 for (i, message) in enumerate(req.chrome[type_]): 382 req.session['chrome.%s.%d' % (type_, i)] = escape(message, False)
383 384 385 # Mappings for removal of control characters 386 _translate_nop = "".join([chr(i) for i in range(256)]) 387 _invalid_control_chars = "".join([chr(i) for i in range(32) 388 if i not in [0x09, 0x0a, 0x0d]]) 389 390
391 -class Chrome(Component):
392 """Web site chrome assembly manager. 393 394 Chrome is everything that is not actual page content. 395 """ 396 397 implements(ISystemInfoProvider, IEnvironmentSetupParticipant, 398 IPermissionRequestor, IRequestHandler, ITemplateProvider, 399 IWikiSyntaxProvider) 400 401 required = True 402 is_valid_default_handler = False 403 404 navigation_contributors = ExtensionPoint(INavigationContributor) 405 template_providers = ExtensionPoint(ITemplateProvider) 406 stream_filters = ExtensionPoint(ITemplateStreamFilter) 407 408 shared_templates_dir = PathOption('inherit', 'templates_dir', '', 409 """Path to the //shared templates directory//. 410 411 Templates in that directory are loaded in addition to those in the 412 environments `templates` directory, but the latter take precedence. 413 414 Non-absolute paths are relative to the Environment `conf` 415 directory. 416 """) 417 418 shared_htdocs_dir = PathOption('inherit', 'htdocs_dir', '', 419 """Path to the //shared htdocs directory//. 420 421 Static resources in that directory are mapped to /chrome/shared 422 under the environment URL, in addition to common and site locations. 423 424 This can be useful in site.html for common interface customization 425 of multiple Trac environments. 426 427 Non-absolute paths are relative to the Environment `conf` 428 directory. 429 (''since 1.0'')""") 430 431 auto_reload = BoolOption('trac', 'auto_reload', False, 432 """Automatically reload template files after modification.""") 433 434 genshi_cache_size = IntOption('trac', 'genshi_cache_size', 128, 435 """The maximum number of templates that the template loader will cache 436 in memory. You may want to choose a higher value if your site uses a 437 larger number of templates, and you have enough memory to spare, or 438 you can reduce it if you are short on memory.""") 439 440 htdocs_location = Option('trac', 'htdocs_location', '', 441 """Base URL for serving the core static resources below 442 `/chrome/common/`. 443 444 It can be left empty, and Trac will simply serve those resources 445 itself. 446 447 Advanced users can use this together with 448 [TracAdmin trac-admin ... deploy <deploydir>] to allow serving the 449 static resources for Trac directly from the web server. 450 Note however that this only applies to the `<deploydir>/htdocs/common` 451 directory, the other deployed resources (i.e. those from plugins) 452 will not be made available this way and additional rewrite 453 rules will be needed in the web server.""") 454 455 jquery_location = Option('trac', 'jquery_location', '', 456 """Location of the jQuery !JavaScript library (version %(version)s). 457 458 An empty value loads jQuery from the copy bundled with Trac. 459 460 Alternatively, jQuery could be loaded from a CDN, for example: 461 http://code.jquery.com/jquery-%(version)s.min.js, 462 http://ajax.aspnetcdn.com/ajax/jQuery/jquery-%(version)s.min.js or 463 https://ajax.googleapis.com/ajax/libs/jquery/%(version)s/jquery.min.js. 464 465 (''since 1.0'')""", doc_args={'version': '1.11.3'}) 466 467 jquery_ui_location = Option('trac', 'jquery_ui_location', '', 468 """Location of the jQuery UI !JavaScript library (version %(version)s). 469 470 An empty value loads jQuery UI from the copy bundled with Trac. 471 472 Alternatively, jQuery UI could be loaded from a CDN, for example: 473 https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/jquery-ui.min.js 474 or 475 http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/jquery-ui.min.js. 476 477 (''since 1.0'')""", doc_args={'version': '1.11.4'}) 478 479 jquery_ui_theme_location = Option('trac', 'jquery_ui_theme_location', '', 480 """Location of the theme to be used with the jQuery UI !JavaScript 481 library (version %(version)s). 482 483 An empty value loads the custom Trac jQuery UI theme from the copy 484 bundled with Trac. 485 486 Alternatively, a jQuery UI theme could be loaded from a CDN, for 487 example: 488 https://ajax.googleapis.com/ajax/libs/jqueryui/%(version)s/themes/start/jquery-ui.css 489 or 490 http://ajax.aspnetcdn.com/ajax/jquery.ui/%(version)s/themes/start/jquery-ui.css. 491 492 (''since 1.0'')""", doc_args={'version': '1.11.4'}) 493 494 mainnav = ConfigSection('mainnav', """Configures the main navigation bar, 495 which by default contains //Wiki//, //Timeline//, //Roadmap//, 496 //Browse Source//, //View Tickets//, //New Ticket//, //Search// and 497 //Admin//. 498 499 The `label`, `href`, and `order` attributes can be specified. Entries 500 can be disabled by setting the value of the navigation item to 501 `disabled`. 502 503 The following example renames the link to WikiStart to //Home//, 504 links the //View Tickets// entry to a specific report and disables 505 the //Search// entry. 506 {{{#!ini 507 [mainnav] 508 wiki.label = Home 509 tickets.href = /report/24 510 search = disabled 511 }}} 512 513 See TracNavigation for more details. 514 """) 515 516 metanav = ConfigSection('metanav', """Configures the meta navigation 517 entries, which by default are //Login//, //Logout//, //Preferences//, 518 ''!Help/Guide'' and //About Trac//. The allowed attributes are the 519 same as for `[mainnav]`. Additionally, a special entry is supported - 520 `logout.redirect` is the page the user sees after hitting the logout 521 button. For example: 522 523 {{{#!ini 524 [metanav] 525 logout.redirect = wiki/Logout 526 }}} 527 528 See TracNavigation for more details. 529 """) 530 531 logo_link = Option('header_logo', 'link', '', 532 """URL to link to, from the header logo.""") 533 534 logo_src = Option('header_logo', 'src', 'site/your_project_logo.png', 535 """URL of the image to use as header logo. 536 It can be absolute, server relative or relative. 537 538 If relative, it is relative to one of the `/chrome` locations: 539 `site/your-logo.png` if `your-logo.png` is located in the `htdocs` 540 folder within your TracEnvironment; 541 `common/your-logo.png` if `your-logo.png` is located in the 542 folder mapped to the [#trac-section htdocs_location] URL. 543 Only specifying `your-logo.png` is equivalent to the latter.""") 544 545 logo_alt = Option('header_logo', 'alt', 546 "(please configure the [header_logo] section in trac.ini)", 547 """Alternative text for the header logo.""") 548 549 logo_width = IntOption('header_logo', 'width', -1, 550 """Width of the header logo image in pixels.""") 551 552 logo_height = IntOption('header_logo', 'height', -1, 553 """Height of the header logo image in pixels.""") 554 555 show_email_addresses = BoolOption('trac', 'show_email_addresses', 'false', 556 """Show email addresses instead of usernames. If false, email 557 addresses are obfuscated for users that don't have EMAIL_VIEW 558 permission. 559 """) 560 561 show_full_names = BoolOption('trac', 'show_full_names', 'true', 562 """Show full names instead of usernames. (//since 1.2//)""") 563 564 never_obfuscate_mailto = BoolOption('trac', 'never_obfuscate_mailto', 565 'false', 566 """Never obfuscate `mailto:` links explicitly written in the wiki, 567 even if `show_email_addresses` is false or the user doesn't have 568 EMAIL_VIEW permission. 569 """) 570 571 show_ip_addresses = BoolOption('trac', 'show_ip_addresses', 'false', 572 """Show IP addresses for resource edits (e.g. wiki). Since 1.0.5 this 573 option is deprecated and will be removed in 1.3.1. 574 """) 575 576 resizable_textareas = BoolOption('trac', 'resizable_textareas', 'true', 577 """Make `<textarea>` fields resizable. Requires !JavaScript. 578 (''since 0.12'')""") 579 580 wiki_toolbars = BoolOption('trac', 'wiki_toolbars', 'true', 581 """Add a simple toolbar on top of Wiki <textarea>s. 582 (''since 1.0.2'')""") 583 584 auto_preview_timeout = FloatOption('trac', 'auto_preview_timeout', 2.0, 585 """Inactivity timeout in seconds after which the automatic wiki preview 586 triggers an update. This option can contain floating-point values. The 587 lower the setting, the more requests will be made to the server. Set 588 this to 0 to disable automatic preview. (''since 0.12'')""") 589 590 default_dateinfo_format = ChoiceOption('trac', 'default_dateinfo_format', 591 ('relative', 'absolute'), 592 """The date information format. Valid options are 'relative' for 593 displaying relative format and 'absolute' for displaying absolute 594 format. (''since 1.0'')""") 595 596 use_chunked_encoding = BoolOption('trac', 'use_chunked_encoding', 'false', 597 """If enabled, send contents as chunked encoding in HTTP/1.1. 598 Otherwise, send contents with `Content-Length` header after entire of 599 the contents are rendered. (''since 1.0.6'')""") 600 601 templates = None 602 603 # DocType for 'text/html' output 604 html_doctype = DocType.XHTML_STRICT 605 606 # A dictionary of default context data for templates 607 _default_context_data = { 608 '_': translation.gettext, 609 'all': all, 610 'any': any, 611 'as_bool': as_bool, 612 'as_int': as_int, 613 'classes': presentation.classes, 614 'date': datetime.date, 615 'datetime': datetime.datetime, 616 'dgettext': translation.dgettext, 617 'dngettext': translation.dngettext, 618 'first_last': presentation.first_last, 619 'find_element': html.find_element, 620 'get_reporter_id': get_reporter_id, 621 'gettext': translation.gettext, 622 'group': presentation.group, 623 'groupby': compat.py_groupby, # http://bugs.python.org/issue2246 624 'http_date': http_date, 625 'is_obfuscated': is_obfuscated, 626 'istext': presentation.istext, 627 'javascript_quote': javascript_quote, 628 'ngettext': translation.ngettext, 629 'operator': operator, 630 'paginate': presentation.paginate, 631 'partial': partial, 632 'pathjoin': pathjoin, 633 'plaintext': plaintext, 634 'pprint': pprint.pformat, 635 'pretty_size': pretty_size, 636 'pretty_timedelta': pretty_timedelta, 637 'quote_plus': unicode_quote_plus, 638 'reversed': reversed, 639 'separated': presentation.separated, 640 'shorten_line': shorten_line, 641 'sorted': sorted, 642 'time': datetime.time, 643 'timedelta': datetime.timedelta, 644 'to_json': presentation.to_json, 645 'to_unicode': to_unicode, 646 'utc': utc, 647 } 648 649 # ISystemInfoProvider methods 650
651 - def get_system_info(self):
652 import genshi 653 info = get_pkginfo(genshi).get('version') 654 if hasattr(genshi, '_speedups'): 655 info += ' (with speedups)' 656 else: 657 info += ' (without speedups)' 658 yield 'Genshi', info 659 try: 660 import babel 661 except ImportError: 662 babel = None 663 if babel is not None: 664 info = get_pkginfo(babel).get('version') 665 if not get_available_locales(): 666 info += " (translations unavailable)" # No i18n on purpose 667 self.log.warning("Locale data is missing") 668 yield 'Babel', info
669 670 # IEnvironmentSetupParticipant methods 671
672 - def environment_created(self):
673 """Create the environment templates directory.""" 674 if self.env.path: 675 templates_dir = os.path.join(self.env.path, 'templates') 676 if not os.path.exists(templates_dir): 677 os.mkdir(templates_dir) 678 679 site_path = os.path.join(templates_dir, 'site.html.sample') 680 with open(site_path, 'w') as fileobj: 681 fileobj.write("""\ 682 <html xmlns="http://www.w3.org/1999/xhtml" 683 xmlns:xi="http://www.w3.org/2001/XInclude" 684 xmlns:py="http://genshi.edgewall.org/" 685 py:strip=""> 686 <!--! 687 This file allows customizing the appearance of the Trac installation. 688 Add your customizations here and rename the file to site.html. Note that 689 it will take precedence over a global site.html placed in the directory 690 specified by [inherit] templates_dir. 691 692 More information about site appearance customization can be found here: 693 694 https://trac.edgewall.org/wiki/TracInterfaceCustomization#SiteAppearance 695 --> 696 </html> 697 """)
698
699 - def environment_needs_upgrade(self):
700 return False
701
702 - def upgrade_environment(self):
703 pass
704 705 # IRequestHandler methods 706
707 - def match_request(self, req):
708 match = re.match(r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>.+)', 709 req.path_info) 710 if match: 711 req.args['prefix'] = match.group('prefix') 712 req.args['filename'] = match.group('filename') 713 return True
714
715 - def process_request(self, req):
716 prefix = req.args['prefix'] 717 filename = req.args['filename'] 718 719 dirs = [] 720 for provider in self.template_providers: 721 for dir in [os.path.normpath(dir[1]) for dir 722 in provider.get_htdocs_dirs() or [] 723 if dir[0] == prefix and dir[1]]: 724 dirs.append(dir) 725 path = os.path.normpath(os.path.join(dir, filename)) 726 if os.path.commonprefix([dir, path]) != dir: 727 raise TracError(_("Invalid chrome path %(path)s.", 728 path=filename)) 729 elif os.path.isfile(path): 730 req.send_file(path, get_mimetype(path)) 731 732 self.log.warning('File %s not found in any of %s', filename, dirs) 733 raise HTTPNotFound('File %s not found', filename)
734 735 # IPermissionRequestor methods 736
737 - def get_permission_actions(self):
738 """`EMAIL_VIEW` permission allows for showing email addresses even 739 if `[trac] show_email_addresses` is `false`.""" 740 return ['EMAIL_VIEW']
741 742 # ITemplateProvider methods 743
744 - def get_htdocs_dirs(self):
745 return [('common', pkg_resources.resource_filename('trac', 'htdocs')), 746 ('shared', self.shared_htdocs_dir), 747 ('site', self.env.htdocs_dir)]
748
749 - def get_templates_dirs(self):
750 return filter(None, [ 751 self.env.templates_dir, 752 self.shared_templates_dir, 753 pkg_resources.resource_filename('trac', 'templates'), 754 ])
755 756 # IWikiSyntaxProvider methods 757
758 - def get_wiki_syntax(self):
759 return []
760 763 768 769 # Public API methods 770
771 - def get_all_templates_dirs(self):
772 """Return a list of the names of all known templates directories.""" 773 dirs = [] 774 for provider in self.template_providers: 775 dirs.extend(provider.get_templates_dirs() or []) 776 return dirs
777
778 - def prepare_request(self, req, handler=None):
779 """Prepare the basic chrome data for the request. 780 781 :param req: the request object 782 :param handler: the `IRequestHandler` instance that is processing the 783 request 784 """ 785 self.log.debug('Prepare chrome data for request') 786 787 chrome = {'metas': [], 'links': {}, 'scripts': [], 'script_data': {}, 788 'ctxtnav': [], 'warnings': [], 'notices': []} 789 req.chrome = chrome 790 791 htdocs_location = self.htdocs_location or req.href.chrome('common') 792 chrome['htdocs_location'] = htdocs_location.rstrip('/') + '/' 793 794 # HTML <head> links 795 add_link(req, 'start', req.href.wiki()) 796 add_link(req, 'search', req.href.search()) 797 add_link(req, 'help', req.href.wiki('TracGuide')) 798 add_stylesheet(req, 'common/css/trac.css') 799 add_script(req, self.jquery_location or 'common/js/jquery.js') 800 # Only activate noConflict mode if requested to by the handler 801 if handler is not None and \ 802 getattr(handler.__class__, 'jquery_noconflict', False): 803 add_script(req, 'common/js/noconflict.js') 804 add_script(req, 'common/js/babel.js') 805 if req.locale is not None and str(req.locale) != 'en_US': 806 add_script(req, 'common/js/messages/%s.js' % req.locale) 807 add_script(req, 'common/js/trac.js') 808 add_script(req, 'common/js/search.js') 809 810 # Shortcut icon 811 chrome['icon'] = self.get_icon_data(req) 812 if chrome['icon']: 813 src = chrome['icon']['src'] 814 mimetype = chrome['icon']['mimetype'] 815 add_link(req, 'icon', src, mimetype=mimetype) 816 817 # Logo image 818 chrome['logo'] = self.get_logo_data(req.href, req.abs_href) 819 820 # Navigation links 821 allitems = {} 822 active = None 823 for contributor in self.navigation_contributors: 824 try: 825 for category, name, text in \ 826 contributor.get_navigation_items(req) or []: 827 category_section = self.config[category] 828 if category_section.getbool(name, True): 829 # the navigation item is enabled (this is the default) 830 item = text if isinstance(text, Element) and \ 831 text.tag.localname == 'a' \ 832 else None 833 label = category_section.get(name + '.label') 834 href = category_section.get(name + '.href') 835 if href and href.startswith('/'): 836 href = req.href + href 837 if item: 838 if label: 839 item.children[0] = label 840 if href: 841 item = item(href=href) 842 else: 843 if href or label: 844 item = tag.a(label or text, href=href) 845 else: 846 item = text 847 allitems.setdefault(category, {})[name] = item 848 if contributor is handler: 849 active = contributor.get_active_navigation_item(req) 850 except Exception as e: 851 name = contributor.__class__.__name__ 852 if isinstance(e, TracError): 853 self.log.warning("Error with navigation contributor %s: " 854 "%s", name, exception_to_unicode(e)) 855 else: 856 self.log.error("Error with navigation contributor %s: %s", 857 name, 858 exception_to_unicode(e, traceback=True)) 859 add_warning(req, _("Error with navigation contributor " 860 '"%(name)s"', name=name)) 861 862 nav = {} 863 for category, navitems in allitems.items(): 864 sect = self.config[category] 865 order = dict((name, sect.getfloat(name + '.order', float('inf'))) 866 for name in navitems) 867 nav[category] = [] 868 for name, label in navitems.items(): 869 nav[category].append({ 870 'name': name, 871 'label': label, 872 'active': name == active 873 }) 874 nav[category].sort(key=lambda e: (order[e['name']], e['name'])) 875 876 chrome['nav'] = nav 877 878 # Default theme file 879 chrome['theme'] = 'theme.html' 880 881 # Avoid recursion by registering as late as possible (#8583) 882 req.add_redirect_listener(_save_messages) 883 884 return chrome
885
886 - def get_icon_data(self, req):
887 icon = {} 888 icon_src = icon_abs_src = self.env.project_icon 889 if icon_src: 890 if not icon_src.startswith('/') and icon_src.find('://') == -1: 891 if '/' in icon_src: 892 icon_abs_src = req.abs_href.chrome(icon_src) 893 icon_src = req.href.chrome(icon_src) 894 else: 895 icon_abs_src = req.abs_href.chrome('common', icon_src) 896 icon_src = req.href.chrome('common', icon_src) 897 mimetype = get_mimetype(icon_src) 898 icon = {'src': icon_src, 'abs_src': icon_abs_src, 899 'mimetype': mimetype} 900 return icon
901
902 - def get_logo_data(self, href, abs_href=None):
903 # TODO: Possibly, links to 'common/' could use chrome.htdocs_location 904 logo = {} 905 logo_src = self.logo_src 906 if logo_src: 907 abs_href = abs_href or href 908 if logo_src.startswith(('http://', 'https://', '/')): 909 # Nothing further can be calculated 910 logo_src_abs = logo_src 911 elif '/' in logo_src: 912 # Like 'common/trac_banner.png' or 'site/my_banner.png' 913 logo_src_abs = abs_href.chrome(logo_src) 914 logo_src = href.chrome(logo_src) 915 else: 916 # Like 'trac_banner.png' 917 logo_src_abs = abs_href.chrome('common', logo_src) 918 logo_src = href.chrome('common', logo_src) 919 width = self.logo_width if self.logo_width > -1 else None 920 height = self.logo_height if self.logo_height > -1 else None 921 logo = { 922 'link': self.logo_link, 'src': logo_src, 923 'src_abs': logo_src_abs, 'alt': self.logo_alt, 924 'width': width, 'height': height 925 } 926 else: 927 logo = {'link': self.logo_link, 'alt': self.logo_alt} 928 return logo
929
930 - def populate_data(self, req, data):
931 d = self._default_context_data.copy() 932 d['trac'] = { 933 'version': self.env.trac_version, 934 'homepage': 'https://trac.edgewall.org/', # FIXME: use setup data 935 } 936 937 href = req and req.href 938 abs_href = req.abs_href if req else self.env.abs_href 939 admin_href = None 940 if self.env.project_admin_trac_url == '.': 941 admin_href = href 942 elif self.env.project_admin_trac_url: 943 admin_href = Href(self.env.project_admin_trac_url) 944 945 d['project'] = { 946 'name': self.env.project_name, 947 'descr': self.env.project_description, 948 'url': self.env.project_url, 949 'admin': self.env.project_admin, 950 'admin_href': admin_href, 951 'admin_trac_url': self.env.project_admin_trac_url, 952 } 953 footer = self.env.project_footer 954 d['chrome'] = { 955 'footer': Markup(footer and translation.gettext(footer)) 956 } 957 if req: 958 d['chrome'].update(req.chrome) 959 else: 960 d['chrome'].update({ 961 'htdocs_location': self.htdocs_location, 962 'logo': self.get_logo_data(self.env.abs_href), 963 }) 964 965 try: 966 show_email_addresses = self.show_email_addresses or \ 967 not req or 'EMAIL_VIEW' in req.perm 968 except Exception as e: 969 # simply log the exception here, as we might already be rendering 970 # the error page 971 self.log.error("Error during check of EMAIL_VIEW: %s", 972 exception_to_unicode(e)) 973 show_email_addresses = False 974 975 def pretty_dateinfo(date, format=None, dateonly=False): 976 if not date: 977 return '' 978 if format == 'date': 979 absolute = user_time(req, format_date, date) 980 else: 981 absolute = user_time(req, format_datetime, date) 982 now = datetime_now(localtz) 983 relative = pretty_timedelta(date, now) 984 if not format: 985 format = req.session.get('dateinfo', 986 self.default_dateinfo_format) 987 in_or_ago = _("in %(relative)s", relative=relative) \ 988 if date > now else \ 989 _("%(relative)s ago", relative=relative) 990 if format == 'relative': 991 label = in_or_ago if not dateonly else relative 992 title = absolute 993 else: 994 if dateonly: 995 label = absolute 996 elif req.lc_time == 'iso8601': 997 label = _("at %(iso8601)s", iso8601=absolute) 998 else: 999 label = _("on %(date)s at %(time)s", 1000 date=user_time(req, format_date, date), 1001 time=user_time(req, format_time, date)) 1002 title = in_or_ago 1003 return tag.span(label, title=title)
1004 1005 def dateinfo(date): 1006 return pretty_dateinfo(date, format='relative', dateonly=True)
1007 1008 def get_rel_url(resource, **kwargs): 1009 return get_resource_url(self.env, resource, href, **kwargs) 1010 1011 def get_abs_url(resource, **kwargs): 1012 return get_resource_url(self.env, resource, abs_href, **kwargs) 1013 1014 dateinfo_format = \ 1015 req.session.get('dateinfo', self.default_dateinfo_format) \ 1016 if req else self.default_dateinfo_format 1017 1018 d.update({ 1019 'context': web_context(req) if req else None, 1020 'Resource': Resource, 1021 'url_of': get_rel_url, 1022 'abs_url_of': get_abs_url, 1023 'name_of': partial(get_resource_name, self.env), 1024 'shortname_of': partial(get_resource_shortname, self.env), 1025 'summary_of': partial(get_resource_summary, self.env), 1026 'resource_link': partial(render_resource_link, self.env), 1027 'req': req, 1028 'abs_href': abs_href, 1029 'href': href, 1030 'perm': req and req.perm, 1031 'authname': req.authname if req else '<trac>', 1032 'locale': req and req.locale, 1033 # show_email_address is deprecated: will be removed in 1.3.1 1034 'show_email_addresses': show_email_addresses, 1035 'show_ip_addresses': self.show_ip_addresses, 1036 'author_email': partial(self.author_email, 1037 email_map=self.get_email_map()), 1038 'authorinfo': partial(self.authorinfo, req), 1039 'authorinfo_short': self.authorinfo_short, 1040 'format_author': partial(self.format_author, req), 1041 'format_emails': self.format_emails, 1042 'get_systeminfo': self.env.get_systeminfo, 1043 'captioned_button': partial(presentation.captioned_button, req), 1044 1045 # Date/time formatting 1046 'dateinfo': dateinfo, 1047 'dateinfo_format': dateinfo_format, 1048 'pretty_dateinfo': pretty_dateinfo, 1049 'format_datetime': partial(user_time, req, format_datetime), 1050 'format_date': partial(user_time, req, format_date), 1051 'format_time': partial(user_time, req, format_time), 1052 'fromtimestamp': partial(datetime.datetime.fromtimestamp, 1053 tz=req and req.tz), 1054 'from_utimestamp': from_utimestamp, 1055 1056 # Wiki-formatting functions 1057 'wiki_to': partial(format_to, self.env), 1058 'wiki_to_html': partial(format_to_html, self.env), 1059 'wiki_to_oneliner': partial(format_to_oneliner, self.env), 1060 }) 1061 1062 # Finally merge in the page-specific data 1063 d.update(data) 1064 return d 1065
1066 - def load_template(self, filename, method=None):
1067 """Retrieve a Template and optionally preset the template data. 1068 1069 Also, if the optional `method` argument is set to `'text'`, a 1070 `NewTextTemplate` instance will be created instead of a 1071 `MarkupTemplate`. 1072 """ 1073 if not self.templates: 1074 self.templates = TemplateLoader( 1075 self.get_all_templates_dirs(), auto_reload=self.auto_reload, 1076 max_cache_size=self.genshi_cache_size, 1077 default_encoding="utf-8", 1078 variable_lookup='lenient', callback=lambda template: 1079 Translator(translation.get_translations()).setup(template)) 1080 1081 if method == 'text': 1082 cls = NewTextTemplate 1083 else: 1084 cls = MarkupTemplate 1085 1086 return self.templates.load(filename, cls=cls)
1087
1088 - def render_template(self, req, filename, data, content_type=None, 1089 fragment=False, iterable=False, method=None):
1090 """Render the `filename` using the `data` for the context. 1091 1092 The `content_type` argument is used to choose the kind of template 1093 used (`NewTextTemplate` if `'text/plain'`, `MarkupTemplate` 1094 otherwise), and tweak the rendering process. Doctype for `'text/html'` 1095 can be specified by setting the `html_doctype` attribute (default 1096 is `XHTML_STRICT`) 1097 1098 The rendering `method` (xml, xhtml or text) may be specified and is 1099 inferred from the `content_type` if not specified. 1100 1101 When `fragment` is specified, the (filtered) Genshi stream is 1102 returned. 1103 1104 When `iterable` is specified, the content as an iterable instance 1105 which is generated from filtered Genshi stream is returned. 1106 """ 1107 if content_type is None: 1108 content_type = 'text/html' 1109 1110 if method is None: 1111 method = {'text/html': 'xhtml', 1112 'text/plain': 'text'}.get(content_type, 'xml') 1113 1114 if method == "xhtml": 1115 # Retrieve post-redirect messages saved in session 1116 for type_ in ['warnings', 'notices']: 1117 try: 1118 for i in itertools.count(): 1119 message = Markup(req.session.pop('chrome.%s.%d' 1120 % (type_, i))) 1121 if message not in req.chrome[type_]: 1122 req.chrome[type_].append(message) 1123 except KeyError: 1124 pass 1125 1126 template = self.load_template(filename, method=method) 1127 data = self.populate_data(req, data) 1128 data['chrome']['content_type'] = content_type 1129 1130 stream = template.generate(**data) 1131 1132 # Filter through ITemplateStreamFilter plugins 1133 if self.stream_filters: 1134 stream |= self._filter_stream(req, method, filename, stream, data) 1135 1136 if fragment: 1137 return stream 1138 1139 if method == 'text': 1140 buffer = StringIO() 1141 stream.render('text', out=buffer, encoding='utf-8') 1142 return buffer.getvalue() 1143 1144 doctype = None 1145 if content_type == 'text/html': 1146 doctype = self.html_doctype 1147 if req.form_token: 1148 stream |= self._add_form_token(req.form_token) 1149 if not req.session.as_int('accesskeys', 0): 1150 stream |= self._strip_accesskeys 1151 1152 links = req.chrome.get('links') 1153 scripts = req.chrome.get('scripts') 1154 script_data = req.chrome.get('script_data') 1155 req.chrome.update({'early_links': links, 'early_scripts': scripts, 1156 'early_script_data': script_data, 1157 'links': {}, 'scripts': [], 'script_data': {}}) 1158 data.setdefault('chrome', {}).update({ 1159 'late_links': req.chrome['links'], 1160 'late_scripts': req.chrome['scripts'], 1161 'late_script_data': req.chrome['script_data'], 1162 }) 1163 1164 if iterable: 1165 return self.iterable_content(stream, method, doctype=doctype) 1166 1167 try: 1168 buffer = StringIO() 1169 stream.render(method, doctype=doctype, out=buffer, 1170 encoding='utf-8') 1171 return buffer.getvalue().translate(_translate_nop, 1172 _invalid_control_chars) 1173 except Exception as e: 1174 # restore what may be needed by the error template 1175 req.chrome.update({'early_links': None, 'early_scripts': None, 1176 'early_script_data': None, 'links': links, 1177 'scripts': scripts, 'script_data': script_data}) 1178 # give some hints when hitting a Genshi unicode error 1179 if isinstance(e, UnicodeError): 1180 pos = self._stream_location(stream) 1181 if pos: 1182 location = "'%s', line %s, char %s" % pos 1183 else: 1184 location = '%s %s' % (filename, 1185 _("(unknown template location)")) 1186 raise TracError(_("Genshi %(error)s error while rendering " 1187 "template %(location)s", 1188 error=e.__class__.__name__, 1189 location=location)) 1190 raise
1191
1192 - def get_interface_customization_files(self):
1193 """Returns a dictionary containing the lists of files present in the 1194 site and shared templates and htdocs directories. 1195 """ 1196 def list_dir(path, suffix=None): 1197 if not os.path.isdir(path): 1198 return [] 1199 return sorted(name for name in os.listdir(path) 1200 if suffix is None or name.endswith(suffix))
1201 1202 files = {} 1203 # Collect templates list 1204 site_templates = list_dir(self.env.templates_dir, '.html') 1205 shared_templates = list_dir(Chrome(self.env).shared_templates_dir, 1206 '.html') 1207 1208 # Collect static resources list 1209 site_htdocs = list_dir(self.env.htdocs_dir) 1210 shared_htdocs = list_dir(Chrome(self.env).shared_htdocs_dir) 1211 1212 if any((site_templates, shared_templates, site_htdocs, shared_htdocs)): 1213 files = { 1214 'site-templates': site_templates, 1215 'shared-templates': shared_templates, 1216 'site-htdocs': site_htdocs, 1217 'shared-htdocs': shared_htdocs, 1218 } 1219 return files 1220
1221 - def iterable_content(self, stream, method, **kwargs):
1222 """Generate an iterable object which iterates `str` instances 1223 from the given stream instance. 1224 1225 :param method: the serialization method; can be either "xml", 1226 "xhtml", "html", "text", or a custom serializer 1227 class 1228 """ 1229 try: 1230 if method == 'text': 1231 for chunk in stream.serialize(method, **kwargs): 1232 yield chunk.encode('utf-8') 1233 else: 1234 for chunk in stream.serialize(method, **kwargs): 1235 yield chunk.encode('utf-8') \ 1236 .translate(_translate_nop, 1237 _invalid_control_chars) 1238 except Exception as e: 1239 pos = self._stream_location(stream) 1240 if pos: 1241 location = "'%s', line %s, char %s" % pos 1242 else: 1243 location = '(unknown template location)' 1244 self.log.error('Genshi %s error while rendering template %s%s', 1245 e.__class__.__name__, location, 1246 exception_to_unicode(e, traceback=True))
1247 1248 # E-mail formatting utilities 1249
1250 - def author_email(self, author, email_map):
1251 """Returns the author email from the `email_map` if `author` 1252 doesn't look like an email address.""" 1253 if email_map and '@' not in author and email_map.get(author): 1254 author = email_map.get(author) 1255 return author
1256
1257 - def authorinfo(self, req, author, email_map=None, resource=None):
1258 """Format a username to HTML. 1259 1260 Calls `Chrome.format_author` to format the username, and wraps 1261 the formatted username in a `span` with class `trac-author`, 1262 `trac-author-anonymous` or `trac-author-none`. 1263 1264 :param req: the `Request` object. 1265 :param author: the author string to be formatted. 1266 :param email_map: dictionary mapping usernames to email addresses. 1267 :param resource: optional `Resource` object for `EMAIL_VIEW` 1268 fine-grained permissions checks. 1269 1270 :since 1.1.6: accepts the optional `resource` keyword parameter. 1271 """ 1272 author = self.author_email(author, email_map) 1273 return tag.span(self.format_author(req, author, resource), 1274 class_=self.author_class(req, author))
1275
1276 - def author_class(self, req, author):
1277 suffix = '' 1278 if author == 'anonymous': 1279 suffix = '-anonymous' 1280 elif not author: 1281 suffix = '-none' 1282 elif req and author == req.authname: 1283 suffix = '-user' 1284 return 'trac-author' + suffix
1285 1286 _long_author_re = re.compile(r'.*<([^@]+)@[^@]+>\s*|([^@]+)@[^@]+') 1287
1288 - def authorinfo_short(self, author):
1289 shortened = None 1290 match = self._long_author_re.match(author or '') 1291 if match: 1292 shortened = match.group(1) or match.group(2) 1293 return self.authorinfo(None, shortened or author)
1294
1295 - def cc_list(self, cc_field):
1296 """Split a CC: value in a list of addresses.""" 1297 ccs = [] 1298 for cc in re.split(r'[;,]', cc_field or ''): 1299 cc = cc.strip() 1300 if cc: 1301 ccs.append(cc) 1302 return ccs
1303
1304 - def format_author(self, req, author, resource=None, show_email=None):
1305 """Format a username in plain text. 1306 1307 If `[trac]` `show_email_addresses` is `False`, email addresses 1308 will be obfuscated when the user doesn't have `EMAIL_VIEW` 1309 (for the resource) and the optional parameter `show_email` is 1310 `None`. Returns translated `anonymous` or `none`, when the 1311 author string is `anonymous` or evaluates to `False`, 1312 respectively. 1313 1314 :param req: a `Request` or `RenderingContext` object. 1315 :param author: the author string to be formatted. 1316 :param resource: an optional `Resource` object for performing 1317 fine-grained permission checks for `EMAIL_VIEW`. 1318 :param show_email: an optional parameter that allows explicit 1319 control of e-mail obfuscation. 1320 1321 :since 1.1.6: accepts the optional `resource` keyword parameter. 1322 :since 1.2: Full name is returned when `[trac]` `show_full_names` 1323 is `True`. 1324 :since 1.2: Email addresses are obfuscated when 1325 `show_email_addresses` is False and `req` is Falsy. 1326 Previously email addresses would not be obfuscated 1327 whenever `req` was Falsy (typically `None`). 1328 """ 1329 if author == 'anonymous': 1330 return _("anonymous") 1331 if not author: 1332 return _("(none)") 1333 users = self.env.get_known_users(as_dict=True) 1334 if self.show_full_names and author in users: 1335 name = users[author][0] 1336 if name: 1337 return name 1338 if show_email is None: 1339 show_email = self.show_email_addresses 1340 if not show_email and req: 1341 show_email = 'EMAIL_VIEW' in req.perm(resource) 1342 return author if show_email else obfuscate_email_address(author)
1343
1344 - def format_emails(self, context, value, sep=', '):
1345 """Normalize a list of e-mails and obfuscate them if needed. 1346 1347 :param context: the context in which the check for obfuscation should 1348 be done 1349 :param value: a string containing a comma-separated list of e-mails 1350 :param sep: the separator to use when rendering the list again 1351 """ 1352 formatted = [self.format_author(context, author) 1353 for author in self.cc_list(value)] 1354 return sep.join(formatted)
1355
1356 - def get_email_map(self):
1357 """Get the email addresses of all known users.""" 1358 email_map = {} 1359 if self.show_email_addresses: 1360 for username, name, email in self.env.get_known_users(): 1361 if email: 1362 email_map[username] = email 1363 return email_map
1364 1365 # Element modifiers 1366
1367 - def add_textarea_grips(self, req):
1368 """Make `<textarea class="trac-resizable">` fields resizable if enabled 1369 by configuration.""" 1370 if self.resizable_textareas: 1371 add_script(req, 'common/js/resizer.js')
1372
1373 - def add_wiki_toolbars(self, req):
1374 """Add wiki toolbars to `<textarea class="wikitext">` fields.""" 1375 if self.wiki_toolbars: 1376 add_script(req, 'common/js/wikitoolbar.js') 1377 self.add_textarea_grips(req)
1378
1379 - def add_auto_preview(self, req):
1380 """Setup auto-preview for `<textarea>` fields.""" 1381 add_script(req, 'common/js/auto_preview.js') 1382 add_script_data(req, auto_preview_timeout=self.auto_preview_timeout, 1383 form_token=req.form_token)
1384
1385 - def add_jquery_ui(self, req):
1386 """Add a reference to the jQuery UI script and link the stylesheet.""" 1387 add_script(req, self.jquery_ui_location 1388 or 'common/js/jquery-ui.js') 1389 add_stylesheet(req, self.jquery_ui_theme_location 1390 or 'common/css/jquery-ui/jquery-ui.css') 1391 add_script(req, 'common/js/jquery-ui-addons.js') 1392 add_stylesheet(req, 'common/css/jquery-ui-addons.css') 1393 is_iso8601 = req.lc_time == 'iso8601' 1394 now = datetime_now(req.tz) 1395 tzoffset = now.strftime('%z') 1396 if is_iso8601: 1397 default_timezone = (-1 if tzoffset.startswith('-') else 1) * \ 1398 (int(tzoffset[1:3]) * 60 + int(tzoffset[3:5])) 1399 timezone_list = get_timezone_list_jquery_ui(now) 1400 else: 1401 default_timezone = None 1402 timezone_list = None 1403 add_script_data(req, jquery_ui={ 1404 'month_names': get_month_names_jquery_ui(req), 1405 'day_names': get_day_names_jquery_ui(req), 1406 'date_format': get_date_format_jquery_ui(req.lc_time), 1407 'time_format': get_time_format_jquery_ui(req.lc_time), 1408 'ampm': not is_24_hours(req.lc_time), 1409 'period_names': get_period_names_jquery_ui(req), 1410 'first_week_day': get_first_week_day_jquery_ui(req), 1411 'timepicker_separator': get_timepicker_separator_jquery_ui(req), 1412 'show_timezone': is_iso8601, 1413 'default_timezone': default_timezone, 1414 'timezone_list': timezone_list, 1415 'timezone_iso8601': is_iso8601, 1416 }) 1417 add_script(req, 'common/js/jquery-ui-i18n.js')
1418 1419 # Template filters 1420
1421 - def _add_form_token(self, token):
1422 elem = tag.div( 1423 tag.input(type='hidden', name='__FORM_TOKEN', value=token) 1424 ) 1425 def _generate(stream, ctxt=None): 1426 for kind, data, pos in stream: 1427 if kind is START and data[0].localname == 'form' \ 1428 and data[1].get('method', '').lower() == 'post': 1429 yield kind, data, pos 1430 for event in elem.generate(): 1431 yield event 1432 else: 1433 yield kind, data, pos
1434 return _generate 1435
1436 - def _strip_accesskeys(self, stream, ctxt=None):
1437 for kind, data, pos in stream: 1438 if kind is START and 'accesskey' in data[1]: 1439 data = data[0], Attrs([(k, v) for k, v in data[1] 1440 if k != 'accesskey']) 1441 yield kind, data, pos
1442
1443 - def _filter_stream(self, req, method, filename, stream, data):
1444 def inner(stream, ctxt=None): 1445 for filter in self.stream_filters: 1446 stream = filter.filter_stream(req, method, filename, stream, 1447 data) 1448 return stream
1449 return inner 1450
1451 - def _stream_location(self, stream):
1452 for kind, data, pos in stream: 1453 return pos
1454