1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.web import resource, static
22 from twisted.python import log
23 from buildbot.status import builder
24 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY
25 from buildbot import version, util
26 from buildbot.process.properties import Properties
27
29 """I represent a box in the top row of the waterfall display: the one
30 which shows the status of the last build for each builder."""
32 """Return a Box instance, which can produce a <td> cell.
33 """
34
36 """I represent the 'current activity' box, just above the builder name."""
38 """Return a Box instance, which can produce a <td> cell.
39 """
40
41 -class IBox(Interface):
42 """I represent a box in the waterfall display."""
44 """Return a Box instance, which wraps an Event and can produce a <td>
45 cell.
46 """
47
50
51 css_classes = {SUCCESS: "success",
52 WARNINGS: "warnings",
53 FAILURE: "failure",
54 SKIPPED: "skipped",
55 EXCEPTION: "exception",
56 RETRY: "retry",
57 None: "",
58 }
59
60
62 """
63 Fetch custom build properties from the HTTP request of a "Force build" or
64 "Resubmit build" HTML form.
65 Check the names for valid strings, and return None if a problem is found.
66 Return a new Properties object containing each property found in req.
67 """
68 properties = Properties()
69 i = 1
70 while True:
71 pname = req.args.get("property%dname" % i, [""])[0]
72 pvalue = req.args.get("property%dvalue" % i, [""])[0]
73 if not pname:
74 break
75 if not re.match(r'^[\w\.\-\/\~:]*$', pname) \
76 or not re.match(r'^[\w\.\-\/\~:]*$', pvalue):
77 log.msg("bad property name='%s', value='%s'" % (pname, pvalue))
78 return None
79 properties.setProperty(pname, pvalue, "Force Build Form")
80 i = i + 1
81
82 return properties
83
85 """
86 Return the class to use for a finished build or buildstep,
87 based on the result.
88 """
89
90 result = b.getResults()
91
92 if isinstance(b, builder.BuildStatus):
93 result = b.getResults()
94 elif isinstance(b, builder.BuildStepStatus):
95 result = b.getResults()[0]
96
97 if isinstance(result, tuple):
98 result = result[0]
99 else:
100 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
101
102 if result == None:
103
104 return "running"
105 return builder.Results[result]
106
108
109
110
111
112 if request.prepath:
113 segs = len(request.prepath) - 1
114 else:
115 segs = 0
116 root = "../" * segs
117 return root
118
121
123 return (path_to_root(request) +
124 "builders/" +
125 urllib.quote(builderstatus.getName(), safe=''))
126
130
134
139
143
145
146
147
148 spacer = False
149 - def __init__(self, text=[], class_=None, urlbase=None,
150 **parms):
151 self.text = text
152 self.class_ = class_
153 self.urlbase = urlbase
154 self.show_idle = 0
155 if parms.has_key('show_idle'):
156 del parms['show_idle']
157 self.show_idle = 1
158
159 self.parms = parms
160
161
162
163 - def td(self, **props):
164 props.update(self.parms)
165 text = self.text
166 if not text and self.show_idle:
167 text = ["[idle]"]
168 props['class'] = self.class_
169 props['text'] = text;
170 return props
171
172
173 -class ContextMixin(object):
174 - def getContext(self, request):
175 status = self.getStatus(request)
176 rootpath = path_to_root(request)
177 locale_enc = locale.getdefaultlocale()[1]
178 if locale_enc is not None:
179 locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc)
180 else:
181 locale_tz = unicode(time.tzname[time.localtime()[-1]])
182 return dict(project_url = status.getProjectURL(),
183 project_name = status.getProjectName(),
184 stylesheet = rootpath + 'default.css',
185 path_to_root = rootpath,
186 version = version,
187 time = time.strftime("%a %d %b %Y %H:%M:%S",
188 time.localtime(util.now())),
189 tz = locale_tz,
190 metatags = [],
191 title = self.getTitle(request),
192 welcomeurl = rootpath,
193 authz = self.getAuthz(request),
194 )
195
196 - def getStatus(self, request):
197 return request.site.buildbot_service.getStatus()
198
199 - def getTitle(self, request):
201
202 - def getAuthz(self, request):
203 return request.site.buildbot_service.authz
204
205 - def getBuildmaster(self, request):
206 return request.site.buildbot_service.master
207
208
210
211 contentType = "text/html; charset=utf-8"
212 title = "Buildbot"
213 addSlash = False
214
216 if self.addSlash and path == "" and len(request.postpath) == 0:
217 return self
218 return resource.Resource.getChild(self, path, request)
219
220
221 - def content(self, req, context):
222 """
223 Generate content using the standard layout and the result of the C{body}
224 method.
225
226 This is suitable for the case where a resource just wants to generate
227 the body of a page. It depends on another method, C{body}, being
228 defined to accept the request object and return a C{str}. C{render}
229 will call this method and to generate the response body.
230 """
231 body = self.body(req)
232 context['content'] = body
233 template = req.site.buildbot_service.templates.get_template(
234 "empty.html")
235 return template.render(**context)
236
237
239
240
241
242
243
244 if hasattr(request, "channel"):
245
246 request.site.buildbot_service.registerChannel(request.channel)
247
248
249
250
251
252 if False and self.addSlash and request.prepath[-1] != '':
253
254
255
256
257
258
259
260
261
262
263
264 url = request.prePathURL()
265 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
266 new_url = request.prepath[-1] + "/"
267 if query:
268 new_url += "?" + query
269 request.redirect(new_url)
270 return ''
271
272 ctx = self.getContext(request)
273
274 data = self.content(request, ctx)
275 if isinstance(data, unicode):
276 data = data.encode("utf-8")
277 request.setHeader("content-type", self.contentType)
278 if request.method == "HEAD":
279 request.setHeader("content-length", len(data))
280 return ''
281 return data
282
283
289 - def content(self, request, cxt):
290 cxt['content'] = self.bodyHTML
291 cxt['title'] = self.title
292 template = request.site.buildbot_service.templates.get_template("empty.html")
293 return template.render(**cxt)
294
295
296
297 have_DirectoryLister = False
298 if hasattr(static, 'DirectoryLister'):
299 have_DirectoryLister = True
301 """This variant of the static.DirectoryLister uses a template
302 for rendering."""
303
304 title = 'BuildBot'
305
307 cxt = self.getContext(request)
308
309 if self.dirs is None:
310 directory = os.listdir(self.path)
311 directory.sort()
312 else:
313 directory = self.dirs
314
315 dirs, files = self._getFilesAndDirectories(directory)
316
317 cxt['path'] = cgi.escape(urllib.unquote(request.uri))
318 cxt['directories'] = dirs
319 cxt['files'] = files
320 template = request.site.buildbot_service.templates.get_template("directory.html")
321 data = template.render(**cxt)
322 if isinstance(data, unicode):
323 data = data.encode("utf-8")
324 return data
325
327 """This class adds support for templated directory
328 views."""
329
331 if have_DirectoryLister:
332 return DirectoryLister(self.path,
333 self.listNames(),
334 self.contentTypes,
335 self.contentEncodings,
336 self.defaultType)
337 else:
338 return static.Data("""
339 Directory Listings require Twisted-9.0.0 or later
340 """, "text/plain")
341
342
343 MINUTE = 60
344 HOUR = 60*MINUTE
345 DAY = 24*HOUR
346 WEEK = 7*DAY
347 MONTH = 30*DAY
348
350 if int(num) == 1:
351 return "%d %s" % (num, word)
352 else:
353 return "%d %s" % (num, words)
354
356 if age <= 90:
357 return "%s ago" % plural("second", "seconds", age)
358 if age < 90*MINUTE:
359 return "about %s ago" % plural("minute", "minutes", age / MINUTE)
360 if age < DAY:
361 return "about %s ago" % plural("hour", "hours", age / HOUR)
362 if age < 2*WEEK:
363 return "about %s ago" % plural("day", "days", age / DAY)
364 if age < 2*MONTH:
365 return "about %s ago" % plural("week", "weeks", age / WEEK)
366 return "a long time ago"
367
368
370 LINE_TIME_FORMAT = "%b %d %H:%M"
371
373 '''
374 Collect the data needed for each line display
375 '''
376 builder_name = build.getBuilder().getName()
377 results = build.getResults()
378 text = build.getText()
379 try:
380 rev = build.getProperty("got_revision")
381 if rev is None:
382 rev = "??"
383 except KeyError:
384 rev = "??"
385 rev = str(rev)
386 css_class = css_classes.get(results, "")
387 repo = build.getSourceStamp().repository
388
389 if type(text) == list:
390 text = " ".join(text)
391
392 values = {'class': css_class,
393 'builder_name': builder_name,
394 'buildnum': build.getNumber(),
395 'results': css_class,
396 'text': " ".join(build.getText()),
397 'buildurl': path_to_build(req, build),
398 'builderurl': path_to_builder(req, build.getBuilder()),
399 'rev': rev,
400 'rev_repo' : repo,
401 'time': time.strftime(self.LINE_TIME_FORMAT,
402 time.localtime(build.getTimes()[0])),
403 'text': text,
404 'include_builder': include_builder
405 }
406 return values
407
409
410
411
412
413
414 if "trunk" in branches:
415 return branches + [None]
416 return branches
417
418
419
420
421 -def createJinjaEnv(revlink=None, changecommentlink=None,
422 repositories=None, projects=None):
423 ''' Create a jinja environment changecommentlink is used to
424 render HTML in the WebStatus and for mail changes
425
426 @type changecommentlink: C{None}, tuple (2 or 3 strings), dict (string -> 2- or 3-tuple) or callable
427 @param changecommentlink: see changelinkfilter()
428
429 @type revlink: C{None}, format-string, dict (repository -> format string) or callable
430 @param revlink: see revlinkfilter()
431
432 @type repositories: C{None} or dict (string -> url)
433 @param repositories: an (optinal) mapping from repository identifiers
434 (as given by Change sources) to URLs. Is used to create a link
435 on every place where a repository is listed in the WebStatus.
436
437 @type projects: C{None} or dict (string -> url)
438 @param projects: similar to repositories, but for projects.
439 '''
440
441
442 assert not hasattr(sys, "frozen"), 'Frozen config not supported with jinja (yet)'
443
444 default_loader = jinja2.PackageLoader('buildbot.status.web', 'templates')
445 root = os.path.join(os.getcwd(), 'templates')
446 loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(root),
447 default_loader])
448 env = jinja2.Environment(loader=loader,
449 extensions=['jinja2.ext.i18n'],
450 trim_blocks=True,
451 undefined=AlmostStrictUndefined)
452
453 env.install_null_translations()
454
455 env.filters.update(dict(
456 urlencode = urllib.quote,
457 email = emailfilter,
458 user = userfilter,
459 shortrev = shortrevfilter(revlink, env),
460 revlink = revlinkfilter(revlink, env),
461 changecomment = changelinkfilter(changecommentlink),
462 repolink = dictlinkfilter(repositories),
463 projectlink = dictlinkfilter(projects)
464 ))
465
466 return env
467
469 ''' Escape & obfuscate e-mail addresses
470
471 replacing @ with <span style="display:none> reportedly works well against web-spiders
472 and the next level is to use rot-13 (or something) and decode in javascript '''
473
474 user = jinja2.escape(value)
475 obfuscator = jinja2.Markup('<span style="display:none">ohnoyoudont</span>@')
476 output = user.replace('@', obfuscator)
477 return output
478
479
481 ''' Hide e-mail address from user name when viewing changes
482
483 We still include the (obfuscated) e-mail so that we can show
484 it on mouse-over or similar etc
485 '''
486 r = re.compile('(.*) +<(.*)>')
487 m = r.search(value)
488 if m:
489 user = jinja2.escape(m.group(1))
490 email = emailfilter(m.group(2))
491 return jinja2.Markup('<div class="user">%s<div class="email">%s</div></div>' % (user, email))
492 else:
493 return emailfilter(value)
494
496 '''Helper function that returns suitable macros and functions
497 for building revision links depending on replacement mechanism
498 '''
499
500 assert not replace or callable(replace) or isinstance(replace, dict) or \
501 isinstance(replace, str) or isinstance(replace, unicode)
502
503 if not replace:
504 return lambda rev, repo: None
505 else:
506 if callable(replace):
507 return lambda rev, repo: replace(rev, repo)
508 elif isinstance(replace, dict):
509 def filter(rev, repo):
510 url = replace.get(repo)
511 if url:
512 return url % urllib.quote(rev)
513 else:
514 return None
515
516 return filter
517 else:
518 return lambda rev, repo: replace % urllib.quote(rev)
519
520 assert False, '_replace has a bad type, but we should never get here'
521
522
524 '''return macros for use with revision links, depending
525 on whether revlinks are configured or not'''
526
527 macros = templates.get_template("revmacros.html").module
528
529 if not replace:
530 id = macros.id
531 short = macros.shorten
532 else:
533 id = macros.id_replace
534 short = macros.shorten_replace
535
536 return (id, short)
537
538
540 ''' Returns a function which shortens the revisison string
541 to 12-chars (chosen as this is the Mercurial short-id length)
542 and add link if replacement string is set.
543
544 (The full id is still visible in HTML, for mouse-over events etc.)
545
546 @param replace: see revlinkfilter()
547 @param templates: a jinja2 environment
548 '''
549
550 url_f = _revlinkcfg(replace, templates)
551
552 def filter(rev, repo):
553 if not rev:
554 return u''
555
556 id_html, short_html = _revlinkmacros(replace, templates)
557 rev = unicode(rev)
558 url = url_f(rev, repo)
559 rev = jinja2.escape(rev)
560 shortrev = rev[:12]
561
562 if shortrev == rev:
563 if url:
564 return id_html(rev=rev, url=url)
565 else:
566 return rev
567 else:
568 if url:
569 return short_html(short=shortrev, rev=rev, url=url)
570 else:
571 return shortrev + '...'
572
573 return filter
574
575
577 ''' Returns a function which adds an url link to a
578 revision identifiers.
579
580 Takes same params as shortrevfilter()
581
582 @param replace: either a python format string with an %s,
583 or a dict mapping repositories to format strings,
584 or a callable taking (revision, repository) arguments
585 and return an URL (or None, if no URL is available),
586 or None, in which case revisions do not get decorated
587 with links
588
589 @param templates: a jinja2 environment
590 '''
591
592 url_f = _revlinkcfg(replace, templates)
593
594 def filter(rev, repo):
595 if not rev:
596 return u''
597
598 rev = unicode(rev)
599 url = url_f(rev, repo)
600 if url:
601 id_html, _ = _revlinkmacros(replace, templates)
602 return id_html(rev=rev, url=url)
603 else:
604 return jinja2.escape(rev)
605
606 return filter
607
608
610 ''' Returns function that does regex search/replace in
611 comments to add links to bug ids and similar.
612
613 @param changelink:
614 Either C{None}
615 or: a tuple (2 or 3 elements)
616 1. a regex to match what we look for
617 2. an url with regex refs (\g<0>, \1, \2, etc) that becomes the 'href' attribute
618 3. (optional) an title string with regex ref regex
619 or: a dict mapping projects to above tuples
620 (no links will be added if the project isn't found)
621 or: a callable taking (changehtml, project) args
622 (where the changetext is HTML escaped in the
623 form of a jinja2.Markup instance) and
624 returning another jinja2.Markup instance with
625 the same change text plus any HTML tags added to it.
626 '''
627
628 assert not changelink or isinstance(changelink, dict) or \
629 isinstance(changelink, tuple) or callable(changelink)
630
631 def replace_from_tuple(t):
632 search, url_replace = t[:2]
633 if len(t) == 3:
634 title_replace = t[2]
635 else:
636 title_replace = ''
637
638 search_re = re.compile(search)
639
640 def replacement_unmatched(text):
641 return jinja2.escape(text)
642 def replacement_matched(mo):
643
644 url = jinja2.escape(mo.expand(url_replace))
645 title = jinja2.escape(mo.expand(title_replace))
646 body = jinja2.escape(mo.group())
647 if title:
648 return '<a href="%s" title="%s">%s</a>' % (url, title, body)
649 else:
650 return '<a href="%s">%s</a>' % (url, body)
651
652 def filter(text, project):
653
654
655
656
657 html = []
658 last_idx = 0
659 for mo in search_re.finditer(text):
660 html.append(replacement_unmatched(text[last_idx:mo.start()]))
661 html.append(replacement_matched(mo))
662 last_idx = mo.end()
663 html.append(replacement_unmatched(text[last_idx:]))
664 return jinja2.Markup(''.join(html))
665
666 return filter
667
668 if not changelink:
669 return lambda text, project: jinja2.escape(text)
670
671 elif isinstance(changelink, dict):
672 def dict_filter(text, project):
673
674
675
676 t = changelink.get(project)
677 if t:
678 return replace_from_tuple(t)(text, project)
679 else:
680 return cgi.escape(text)
681
682 return dict_filter
683
684 elif isinstance(changelink, tuple):
685 return replace_from_tuple(changelink)
686
687 elif callable(changelink):
688 def callable_filter(text, project):
689 text = jinja2.escape(text)
690 return changelink(text, project)
691
692 return callable_filter
693
694 assert False, 'changelink has unsupported type, but that is checked before'
695
696
698 '''A filter that encloses the given value in a link tag
699 given that the value exists in the dictionary'''
700
701 assert not links or callable(links) or isinstance(links, dict)
702
703 if not links:
704 return jinja2.escape
705
706 def filter(key):
707 if callable(links):
708 url = links(key)
709 else:
710 url = links.get(key)
711
712 safe_key = jinja2.escape(key)
713
714 if url:
715 return jinja2.Markup(r'<a href="%s">%s</a>' % (url, safe_key))
716 else:
717 return safe_key
718
719 return filter
720
722 ''' An undefined that allows boolean testing but
723 fails properly on every other use.
724
725 Much better than the default Undefined, but not
726 fully as strict as StrictUndefined '''
729