Package buildbot :: Package status :: Package web :: Module base
[frames] | no frames]

Source Code for Module buildbot.status.web.base

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import urlparse, urllib, time, re 
 18  import os, cgi, sys, locale 
 19  import jinja2 
 20  from zope.interface import Interface 
 21  from twisted.internet import defer 
 22  from twisted.web import resource, static, server 
 23  from twisted.python import log 
 24  from buildbot.status import builder, buildstep, build 
 25  from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED 
 26  from buildbot.status.results import EXCEPTION, RETRY 
 27  from buildbot import version, util 
 28  from buildbot.process.properties import Properties 
 29   
30 -class ITopBox(Interface):
31 """I represent a box in the top row of the waterfall display: the one 32 which shows the status of the last build for each builder."""
33 - def getBox(self, request):
34 """Return a Box instance, which can produce a <td> cell. 35 """
36
37 -class ICurrentBox(Interface):
38 """I represent the 'current activity' box, just above the builder name."""
39 - def getBox(self, status):
40 """Return a Box instance, which can produce a <td> cell. 41 """
42
43 -class IBox(Interface):
44 """I represent a box in the waterfall display."""
45 - def getBox(self, request):
46 """Return a Box instance, which wraps an Event and can produce a <td> 47 cell. 48 """
49
50 -class IHTMLLog(Interface):
51 pass
52 53 css_classes = {SUCCESS: "success", 54 WARNINGS: "warnings", 55 FAILURE: "failure", 56 SKIPPED: "skipped", 57 EXCEPTION: "exception", 58 RETRY: "retry", 59 None: "", 60 } 61 62
63 -def getAndCheckProperties(req):
64 """ 65 Fetch custom build properties from the HTTP request of a "Force build" or 66 "Resubmit build" HTML form. 67 Check the names for valid strings, and return None if a problem is found. 68 Return a new Properties object containing each property found in req. 69 """ 70 master = req.site.buildbot_service.master 71 pname_validate = master.config.validation['property_name'] 72 pval_validate = master.config.validation['property_value'] 73 properties = Properties() 74 i = 1 75 while True: 76 pname = req.args.get("property%dname" % i, [""])[0] 77 pvalue = req.args.get("property%dvalue" % i, [""])[0] 78 if not pname: 79 break 80 if not pname_validate.match(pname) \ 81 or not pval_validate.match(pvalue): 82 log.msg("bad property name='%s', value='%s'" % (pname, pvalue)) 83 return None 84 properties.setProperty(pname, pvalue, "Force Build Form") 85 i = i + 1 86 87 return properties
88
89 -def build_get_class(b):
90 """ 91 Return the class to use for a finished build or buildstep, 92 based on the result. 93 """ 94 # FIXME: this getResults duplicity might need to be fixed 95 result = b.getResults() 96 if isinstance(b, build.BuildStatus): 97 result = b.getResults() 98 elif isinstance(b, buildstep.BuildStepStatus): 99 result = b.getResults()[0] 100 # after forcing a build, b.getResults() returns ((None, []), []), ugh 101 if isinstance(result, tuple): 102 result = result[0] 103 else: 104 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b 105 106 if result == None: 107 # FIXME: this happens when a buildstep is running ? 108 return "running" 109 return builder.Results[result]
110
111 -def path_to_root(request):
112 # /waterfall : ['waterfall'] -> '' 113 # /somewhere/lower : ['somewhere', 'lower'] -> '../' 114 # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../' 115 # / : [] -> '' 116 if request.prepath: 117 segs = len(request.prepath) - 1 118 else: 119 segs = 0 120 root = "../" * segs 121 return root
122
123 -def path_to_authfail(request):
124 return path_to_root(request) + "authfail"
125
126 -def path_to_authzfail(request):
127 return path_to_root(request) + "authzfail"
128
129 -def path_to_builder(request, builderstatus):
130 return (path_to_root(request) + 131 "builders/" + 132 urllib.quote(builderstatus.getName(), safe=''))
133
134 -def path_to_build(request, buildstatus):
135 return (path_to_builder(request, buildstatus.getBuilder()) + 136 "/builds/%d" % buildstatus.getNumber())
137
138 -def path_to_step(request, stepstatus):
139 return (path_to_build(request, stepstatus.getBuild()) + 140 "/steps/%s" % urllib.quote(stepstatus.getName(), safe=''))
141
142 -def path_to_slave(request, slave):
143 return (path_to_root(request) + 144 "buildslaves/" + 145 urllib.quote(slave.getName(), safe=''))
146
147 -def path_to_change(request, change):
148 return (path_to_root(request) + 149 "changes/%s" % change.number)
150
151 -class Box:
152 # a Box wraps an Event. The Box has HTML <td> parameters that Events 153 # lack, and it has a base URL to which each File's name is relative. 154 # Events don't know about HTML. 155 spacer = False
156 - def __init__(self, text=[], class_=None, urlbase=None, 157 **parms):
158 self.text = text 159 self.class_ = class_ 160 self.urlbase = urlbase 161 self.show_idle = 0 162 if parms.has_key('show_idle'): 163 del parms['show_idle'] 164 self.show_idle = 1 165 166 self.parms = parms
167 # parms is a dict of HTML parameters for the <td> element that will 168 # represent this Event in the waterfall display. 169
170 - def td(self, **props):
171 props.update(self.parms) 172 text = self.text 173 if not text and self.show_idle: 174 text = ["[idle]"] 175 props['class'] = self.class_ 176 props['text'] = text; 177 return props
178 179
180 -class AccessorMixin(object):
181 - def getStatus(self, request):
182 return request.site.buildbot_service.getStatus()
183
184 - def getPageTitle(self, request):
185 return self.pageTitle
186
187 - def getAuthz(self, request):
188 return request.site.buildbot_service.authz
189
190 - def getBuildmaster(self, request):
191 return request.site.buildbot_service.master
192 193
194 -class ContextMixin(AccessorMixin):
195 - def getContext(self, request):
196 status = self.getStatus(request) 197 rootpath = path_to_root(request) 198 locale_enc = locale.getdefaultlocale()[1] 199 if locale_enc is not None: 200 locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc) 201 else: 202 locale_tz = unicode(time.tzname[time.localtime()[-1]]) 203 return dict(title_url = status.getTitleURL(), 204 title = status.getTitle(), 205 stylesheet = rootpath + 'default.css', 206 path_to_root = rootpath, 207 version = version, 208 time = time.strftime("%a %d %b %Y %H:%M:%S", 209 time.localtime(util.now())), 210 tz = locale_tz, 211 metatags = [], 212 pageTitle = self.getPageTitle(request), 213 welcomeurl = rootpath, 214 authz = self.getAuthz(request), 215 request = request, 216 alert_msg = request.args.get("alert_msg", [""])[0], 217 )
218 219
220 -class ActionResource(resource.Resource, AccessorMixin):
221 """A resource that performs some action, then redirects to a new URL.""" 222 223 isLeaf = 1 224
225 - def getChild(self, name, request):
226 return self
227
228 - def performAction(self, request):
229 """ 230 Perform the action, and return the URL to redirect to 231 232 @param request: the web request 233 @returns: URL via Deferred 234 can also return (URL, alert_msg) to display simple 235 feedback to user in case of failure 236 """
237
238 - def render(self, request):
239 d = defer.maybeDeferred(lambda : self.performAction(request)) 240 def redirect(url): 241 if isinstance(url, tuple): 242 url, alert_msg = url 243 if alert_msg: 244 url += "?alert_msg="+urllib.quote(alert_msg, safe='') 245 request.redirect(url) 246 request.write("see <a href='%s'>%s</a>" % (url,url)) 247 try: 248 request.finish() 249 except RuntimeError: 250 # this occurs when the client has already disconnected; ignore 251 # it (see #2027) 252 log.msg("http client disconnected before results were sent")
253 d.addCallback(redirect) 254 255 def fail(f): 256 request.processingFailed(f) 257 return None # processingFailed will log this for us
258 d.addErrback(fail) 259 return server.NOT_DONE_YET 260
261 -class HtmlResource(resource.Resource, ContextMixin):
262 # this is a cheap sort of template thingy 263 contentType = "text/html; charset=utf-8" 264 pageTitle = "Buildbot" 265 addSlash = False # adapted from Nevow 266
267 - def getChild(self, path, request):
268 if self.addSlash and path == "" and len(request.postpath) == 0: 269 return self 270 return resource.Resource.getChild(self, path, request)
271 272
273 - def content(self, req, context):
274 """ 275 Generate content using the standard layout and the result of the C{body} 276 method. 277 278 This is suitable for the case where a resource just wants to generate 279 the body of a page. It depends on another method, C{body}, being 280 defined to accept the request object and return a C{str}. C{render} 281 will call this method and to generate the response body. 282 """ 283 body = self.body(req) 284 context['content'] = body 285 template = req.site.buildbot_service.templates.get_template( 286 "empty.html") 287 return template.render(**context)
288 289
290 - def render(self, request):
291 # tell the WebStatus about the HTTPChannel that got opened, so they 292 # can close it if we get reconfigured and the WebStatus goes away. 293 # They keep a weakref to this, since chances are good that it will be 294 # closed by the browser or by us before we get reconfigured. See 295 # ticket #102 for details. 296 if hasattr(request, "channel"): 297 # web.distrib.Request has no .channel 298 request.site.buildbot_service.registerChannel(request.channel) 299 300 # Our pages no longer require that their URL end in a slash. Instead, 301 # they all use request.childLink() or some equivalent which takes the 302 # last path component into account. This clause is left here for 303 # historical and educational purposes. 304 if False and self.addSlash and request.prepath[-1] != '': 305 # this is intended to behave like request.URLPath().child('') 306 # but we need a relative URL, since we might be living behind a 307 # reverse proxy 308 # 309 # note that the Location: header (as used in redirects) are 310 # required to have absolute URIs, and my attempt to handle 311 # reverse-proxies gracefully violates rfc2616. This frequently 312 # works, but single-component paths sometimes break. The best 313 # strategy is to avoid these redirects whenever possible by using 314 # HREFs with trailing slashes, and only use the redirects for 315 # manually entered URLs. 316 url = request.prePathURL() 317 scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 318 new_url = request.prepath[-1] + "/" 319 if query: 320 new_url += "?" + query 321 request.redirect(new_url) 322 return '' 323 324 ctx = self.getContext(request) 325 326 d = defer.maybeDeferred(lambda : self.content(request, ctx)) 327 def handle(data): 328 if isinstance(data, unicode): 329 data = data.encode("utf-8") 330 request.setHeader("content-type", self.contentType) 331 if request.method == "HEAD": 332 request.setHeader("content-length", len(data)) 333 return '' 334 return data
335 d.addCallback(handle) 336 def ok(data): 337 request.write(data) 338 try: 339 request.finish() 340 except RuntimeError: 341 # this occurs when the client has already disconnected; ignore 342 # it (see #2027) 343 log.msg("http client disconnected before results were sent")
344 def fail(f): 345 request.processingFailed(f) 346 return None # processingFailed will log this for us 347 d.addCallbacks(ok, fail) 348 return server.NOT_DONE_YET 349
350 -class StaticHTML(HtmlResource):
351 - def __init__(self, body, pageTitle):
352 HtmlResource.__init__(self) 353 self.bodyHTML = body 354 self.pageTitle = pageTitle
355 - def content(self, request, cxt):
356 cxt['content'] = self.bodyHTML 357 cxt['pageTitle'] = self.pageTitle 358 template = request.site.buildbot_service.templates.get_template("empty.html") 359 return template.render(**cxt)
360
361 -class DirectoryLister(static.DirectoryLister, ContextMixin):
362 """This variant of the static.DirectoryLister uses a template 363 for rendering.""" 364 365 pageTitle = 'BuildBot' 366
367 - def render(self, request):
368 cxt = self.getContext(request) 369 370 if self.dirs is None: 371 directory = os.listdir(self.path) 372 directory.sort() 373 else: 374 directory = self.dirs 375 376 dirs, files = self._getFilesAndDirectories(directory) 377 378 cxt['path'] = cgi.escape(urllib.unquote(request.uri)) 379 cxt['directories'] = dirs 380 cxt['files'] = files 381 template = request.site.buildbot_service.templates.get_template("directory.html") 382 data = template.render(**cxt) 383 if isinstance(data, unicode): 384 data = data.encode("utf-8") 385 return data
386
387 -class StaticFile(static.File):
388 """This class adds support for templated directory 389 views.""" 390
391 - def directoryListing(self):
392 return DirectoryLister(self.path, 393 self.listNames(), 394 self.contentTypes, 395 self.contentEncodings, 396 self.defaultType)
397 398 399 MINUTE = 60 400 HOUR = 60*MINUTE 401 DAY = 24*HOUR 402 WEEK = 7*DAY 403 MONTH = 30*DAY 404
405 -def plural(word, words, num):
406 if int(num) == 1: 407 return "%d %s" % (num, word) 408 else: 409 return "%d %s" % (num, words)
410
411 -def abbreviate_age(age):
412 if age <= 90: 413 return "%s ago" % plural("second", "seconds", age) 414 if age < 90*MINUTE: 415 return "about %s ago" % plural("minute", "minutes", age / MINUTE) 416 if age < DAY: 417 return "about %s ago" % plural("hour", "hours", age / HOUR) 418 if age < 2*WEEK: 419 return "about %s ago" % plural("day", "days", age / DAY) 420 if age < 2*MONTH: 421 return "about %s ago" % plural("week", "weeks", age / WEEK) 422 return "a long time ago"
423 424
425 -class BuildLineMixin:
426 LINE_TIME_FORMAT = "%b %d %H:%M" 427
428 - def get_line_values(self, req, build, include_builder=True):
429 ''' 430 Collect the data needed for each line display 431 ''' 432 builder_name = build.getBuilder().getName() 433 results = build.getResults() 434 text = build.getText() 435 all_got_revision = build.getAllGotRevisions() 436 css_class = css_classes.get(results, "") 437 ss_list = build.getSourceStamps() 438 if ss_list: 439 repo = ss_list[0].repository 440 if all_got_revision: 441 if len(ss_list) == 1: 442 rev = all_got_revision.get(ss_list[0].codebase, "??") 443 else: 444 rev = "multiple rev." 445 else: 446 rev = "??" 447 else: 448 repo = 'unknown, no information in build' 449 rev = 'unknown' 450 451 if type(text) == list: 452 text = " ".join(text) 453 454 values = {'class': css_class, 455 'builder_name': builder_name, 456 'buildnum': build.getNumber(), 457 'results': css_class, 458 'text': " ".join(build.getText()), 459 'buildurl': path_to_build(req, build), 460 'builderurl': path_to_builder(req, build.getBuilder()), 461 'rev': rev, 462 'rev_repo' : repo, 463 'time': time.strftime(self.LINE_TIME_FORMAT, 464 time.localtime(build.getTimes()[0])), 465 'text': text, 466 'include_builder': include_builder 467 } 468 return values
469
470 -def map_branches(branches):
471 # when the query args say "trunk", present that to things like 472 # IBuilderStatus.generateFinishedBuilds as None, since that's the 473 # convention in use. But also include 'trunk', because some VC systems 474 # refer to it that way. In the long run we should clean this up better, 475 # maybe with Branch objects or something. 476 if "trunk" in branches: 477 return branches + [None] 478 return branches
479 480 481 # jinja utilities 482
483 -def createJinjaEnv(revlink=None, changecommentlink=None, 484 repositories=None, projects=None, jinja_loaders=None):
485 ''' Create a jinja environment changecommentlink is used to 486 render HTML in the WebStatus and for mail changes 487 488 @type changecommentlink: C{None}, tuple (2 or 3 strings), dict (string -> 2- or 3-tuple) or callable 489 @param changecommentlink: see changelinkfilter() 490 491 @type revlink: C{None}, format-string, dict (repository -> format string) or callable 492 @param revlink: see revlinkfilter() 493 494 @type repositories: C{None} or dict (string -> url) 495 @param repositories: an (optinal) mapping from repository identifiers 496 (as given by Change sources) to URLs. Is used to create a link 497 on every place where a repository is listed in the WebStatus. 498 499 @type projects: C{None} or dict (string -> url) 500 @param projects: similar to repositories, but for projects. 501 ''' 502 503 # See http://buildbot.net/trac/ticket/658 504 assert not hasattr(sys, "frozen"), 'Frozen config not supported with jinja (yet)' 505 506 all_loaders = [jinja2.FileSystemLoader(os.path.join(os.getcwd(), 'templates'))] 507 if jinja_loaders: 508 all_loaders.extend(jinja_loaders) 509 all_loaders.append(jinja2.PackageLoader('buildbot.status.web', 'templates')) 510 loader = jinja2.ChoiceLoader(all_loaders) 511 512 env = jinja2.Environment(loader=loader, 513 extensions=['jinja2.ext.i18n'], 514 trim_blocks=True, 515 undefined=AlmostStrictUndefined) 516 517 env.install_null_translations() # needed until we have a proper i18n backend 518 519 env.tests['mapping'] = lambda obj : isinstance(obj, dict) 520 521 env.filters.update(dict( 522 urlencode = urllib.quote, 523 email = emailfilter, 524 user = userfilter, 525 shortrev = shortrevfilter(revlink, env), 526 revlink = revlinkfilter(revlink, env), 527 changecomment = changelinkfilter(changecommentlink), 528 repolink = dictlinkfilter(repositories), 529 projectlink = dictlinkfilter(projects) 530 )) 531 532 return env
533
534 -def emailfilter(value):
535 ''' Escape & obfuscate e-mail addresses 536 537 replacing @ with <span style="display:none> reportedly works well against web-spiders 538 and the next level is to use rot-13 (or something) and decode in javascript ''' 539 540 user = jinja2.escape(value) 541 obfuscator = jinja2.Markup('<span style="display:none">ohnoyoudont</span>@') 542 output = user.replace('@', obfuscator) 543 return output
544 545
546 -def userfilter(value):
547 ''' Hide e-mail address from user name when viewing changes 548 549 We still include the (obfuscated) e-mail so that we can show 550 it on mouse-over or similar etc 551 ''' 552 r = re.compile('(.*) +<(.*)>') 553 m = r.search(value) 554 if m: 555 user = jinja2.escape(m.group(1)) 556 email = emailfilter(m.group(2)) 557 return jinja2.Markup('<div class="user">%s<div class="email">%s</div></div>' % (user, email)) 558 else: 559 return emailfilter(value) # filter for emails here for safety
560
561 -def _revlinkcfg(replace, templates):
562 '''Helper function that returns suitable macros and functions 563 for building revision links depending on replacement mechanism 564 ''' 565 566 assert not replace or callable(replace) or isinstance(replace, dict) or \ 567 isinstance(replace, str) or isinstance(replace, unicode) 568 569 if not replace: 570 return lambda rev, repo: None 571 else: 572 if callable(replace): 573 return lambda rev, repo: replace(rev, repo) 574 elif isinstance(replace, dict): # TODO: test for [] instead 575 def filter(rev, repo): 576 url = replace.get(repo) 577 if url: 578 return url % urllib.quote(rev) 579 else: 580 return None
581 582 return filter 583 else: 584 return lambda rev, repo: replace % urllib.quote(rev) 585 586 assert False, '_replace has a bad type, but we should never get here' 587 588
589 -def _revlinkmacros(replace, templates):
590 '''return macros for use with revision links, depending 591 on whether revlinks are configured or not''' 592 593 macros = templates.get_template("revmacros.html").module 594 595 if not replace: 596 id = macros.id 597 short = macros.shorten 598 else: 599 id = macros.id_replace 600 short = macros.shorten_replace 601 602 return (id, short)
603 604
605 -def shortrevfilter(replace, templates):
606 ''' Returns a function which shortens the revisison string 607 to 12-chars (chosen as this is the Mercurial short-id length) 608 and add link if replacement string is set. 609 610 (The full id is still visible in HTML, for mouse-over events etc.) 611 612 @param replace: see revlinkfilter() 613 @param templates: a jinja2 environment 614 ''' 615 616 url_f = _revlinkcfg(replace, templates) 617 618 def filter(rev, repo): 619 if not rev: 620 return u'' 621 622 id_html, short_html = _revlinkmacros(replace, templates) 623 rev = unicode(rev) 624 url = url_f(rev, repo) 625 rev = jinja2.escape(rev) 626 shortrev = rev[:12] # TODO: customize this depending on vc type 627 628 if shortrev == rev: 629 if url: 630 return id_html(rev=rev, url=url) 631 else: 632 return rev 633 else: 634 if url: 635 return short_html(short=shortrev, rev=rev, url=url) 636 else: 637 return shortrev + '...'
638 639 return filter 640 641
642 -def revlinkfilter(replace, templates):
643 ''' Returns a function which adds an url link to a 644 revision identifiers. 645 646 Takes same params as shortrevfilter() 647 648 @param replace: either a python format string with an %s, 649 or a dict mapping repositories to format strings, 650 or a callable taking (revision, repository) arguments 651 and return an URL (or None, if no URL is available), 652 or None, in which case revisions do not get decorated 653 with links 654 655 @param templates: a jinja2 environment 656 ''' 657 658 url_f = _revlinkcfg(replace, templates) 659 660 def filter(rev, repo): 661 if not rev: 662 return u'' 663 664 rev = unicode(rev) 665 url = url_f(rev, repo) 666 if url: 667 id_html, _ = _revlinkmacros(replace, templates) 668 return id_html(rev=rev, url=url) 669 else: 670 return jinja2.escape(rev)
671 672 return filter 673 674
675 -def changelinkfilter(changelink):
676 ''' Returns function that does regex search/replace in 677 comments to add links to bug ids and similar. 678 679 @param changelink: 680 Either C{None} 681 or: a tuple (2 or 3 elements) 682 1. a regex to match what we look for 683 2. an url with regex refs (\g<0>, \1, \2, etc) that becomes the 'href' attribute 684 3. (optional) an title string with regex ref regex 685 or: a dict mapping projects to above tuples 686 (no links will be added if the project isn't found) 687 or: a callable taking (changehtml, project) args 688 (where the changetext is HTML escaped in the 689 form of a jinja2.Markup instance) and 690 returning another jinja2.Markup instance with 691 the same change text plus any HTML tags added to it. 692 ''' 693 694 assert not changelink or isinstance(changelink, dict) or \ 695 isinstance(changelink, tuple) or callable(changelink) 696 697 def replace_from_tuple(t): 698 search, url_replace = t[:2] 699 if len(t) == 3: 700 title_replace = t[2] 701 else: 702 title_replace = '' 703 704 search_re = re.compile(search) 705 706 def replacement_unmatched(text): 707 return jinja2.escape(text)
708 def replacement_matched(mo): 709 # expand things *after* application of the regular expressions 710 url = jinja2.escape(mo.expand(url_replace)) 711 title = jinja2.escape(mo.expand(title_replace)) 712 body = jinja2.escape(mo.group()) 713 if title: 714 return '<a href="%s" title="%s">%s</a>' % (url, title, body) 715 else: 716 return '<a href="%s">%s</a>' % (url, body) 717 718 def filter(text, project): 719 # now, we need to split the string into matched and unmatched portions, 720 # quoting the unmatched portions directly and quoting the components of 721 # the 'a' element for the matched portions. We can't use re.split here, 722 # because the user-supplied patterns may have multiple groups. 723 html = [] 724 last_idx = 0 725 for mo in search_re.finditer(text): 726 html.append(replacement_unmatched(text[last_idx:mo.start()])) 727 html.append(replacement_matched(mo)) 728 last_idx = mo.end() 729 html.append(replacement_unmatched(text[last_idx:])) 730 return jinja2.Markup(''.join(html)) 731 732 return filter 733 734 if not changelink: 735 return lambda text, project: jinja2.escape(text) 736 737 elif isinstance(changelink, dict): 738 def dict_filter(text, project): 739 # TODO: Optimize and cache return value from replace_from_tuple so 740 # we only compile regex once per project, not per view 741 742 t = changelink.get(project) 743 if t: 744 return replace_from_tuple(t)(text, project) 745 else: 746 return cgi.escape(text) 747 748 return dict_filter 749 750 elif isinstance(changelink, tuple): 751 return replace_from_tuple(changelink) 752 753 elif callable(changelink): 754 def callable_filter(text, project): 755 text = jinja2.escape(text) 756 return changelink(text, project) 757 758 return callable_filter 759 760 assert False, 'changelink has unsupported type, but that is checked before' 761 762
763 -def dictlinkfilter(links):
764 '''A filter that encloses the given value in a link tag 765 given that the value exists in the dictionary''' 766 767 assert not links or callable(links) or isinstance(links, dict) 768 769 if not links: 770 return jinja2.escape 771 772 def filter(key): 773 if callable(links): 774 url = links(key) 775 else: 776 url = links.get(key) 777 778 safe_key = jinja2.escape(key) 779 780 if url: 781 return jinja2.Markup(r'<a href="%s">%s</a>' % (url, safe_key)) 782 else: 783 return safe_key
784 785 return filter 786
787 -class AlmostStrictUndefined(jinja2.StrictUndefined):
788 ''' An undefined that allows boolean testing but 789 fails properly on every other use. 790 791 Much better than the default Undefined, but not 792 fully as strict as StrictUndefined '''
793 - def __nonzero__(self):
794 return False
795 796 _charsetRe = re.compile('charset=([^;]*)', re.I)
797 -def getRequestCharset(req):
798 """Get the charset for an x-www-form-urlencoded request""" 799 # per http://stackoverflow.com/questions/708915/detecting-the-character-encoding-of-an-http-post-request 800 hdr = req.getHeader('Content-Type') 801 if hdr: 802 mo = _charsetRe.search(hdr) 803 if mo: 804 return mo.group(1).strip() 805 return 'utf-8' # reasonable guess, works for ascii
806