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

Source Code for Module buildbot.status.web.base

  1   
  2  import urlparse, urllib, time 
  3  from zope.interface import Interface 
  4  from twisted.web import html, resource 
  5  from buildbot.status import builder 
  6  from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION 
  7  from buildbot import version, util 
  8   
9 -class ITopBox(Interface):
10 """I represent a box in the top row of the waterfall display: the one 11 which shows the status of the last build for each builder."""
12 - def getBox(self, request):
13 """Return a Box instance, which can produce a <td> cell. 14 """
15
16 -class ICurrentBox(Interface):
17 """I represent the 'current activity' box, just above the builder name."""
18 - def getBox(self, status):
19 """Return a Box instance, which can produce a <td> cell. 20 """
21
22 -class IBox(Interface):
23 """I represent a box in the waterfall display."""
24 - def getBox(self, request):
25 """Return a Box instance, which wraps an Event and can produce a <td> 26 cell. 27 """
28
29 -class IHTMLLog(Interface):
30 pass
31 32 css_classes = {SUCCESS: "success", 33 WARNINGS: "warnings", 34 FAILURE: "failure", 35 SKIPPED: "skipped", 36 EXCEPTION: "exception", 37 } 38 39 ROW_TEMPLATE = ''' 40 <div class="row"> 41 <span class="label">%(label)s</span> 42 <span class="field">%(field)s</span> 43 </div> 44 ''' 45
46 -def make_row(label, field):
47 """Create a name/value row for the HTML. 48 49 `label` is plain text; it will be HTML-encoded. 50 51 `field` is a bit of HTML structure; it will not be encoded in 52 any way. 53 """ 54 label = html.escape(label) 55 return ROW_TEMPLATE % {"label": label, "field": field}
56
57 -def make_name_user_passwd_form(useUserPasswd):
58 """helper function to create HTML prompt for 'name' when 59 C{useUserPasswd} is C{False} or 'username' / 'password' prompt 60 when C{True}.""" 61 62 if useUserPasswd: 63 label = "Your username:" 64 else: 65 label = "Your name:" 66 data = make_row(label, '<input type="text" name="username" />') 67 if useUserPasswd: 68 data += make_row("Your password:", 69 '<input type="password" name="passwd" />') 70 return data
71
72 -def make_stop_form(stopURL, useUserPasswd, on_all=False, label="Build"):
73 if on_all: 74 data = """<form method="post" action="%s" class='command stopbuild'> 75 <p>To stop all builds, fill out the following fields and 76 push the 'Stop' button</p>\n""" % stopURL 77 else: 78 data = """<form method="post" action="%s" class='command stopbuild'> 79 <p>To stop this build, fill out the following fields and 80 push the 'Stop' button</p>\n""" % stopURL 81 data += make_name_user_passwd_form(useUserPasswd) 82 data += make_row("Reason for stopping build:", 83 "<input type='text' name='comments' />") 84 data += '<input type="submit" value="Stop %s" /></form>\n' % label 85 return data
86
87 -def make_force_build_form(forceURL, useUserPasswd, on_all=False):
88 if on_all: 89 data = """<form method="post" action="%s" class="command forcebuild"> 90 <p>To force a build on all Builders, fill out the following fields 91 and push the 'Force Build' button</p>""" % forceURL 92 else: 93 data = """<form method="post" action="%s" class="command forcebuild"> 94 <p>To force a build, fill out the following fields and 95 push the 'Force Build' button</p>""" % forceURL 96 return (data 97 + make_name_user_passwd_form(useUserPasswd) 98 + make_row("Reason for build:", 99 "<input type='text' name='comments' />") 100 + make_row("Branch to build:", 101 "<input type='text' name='branch' />") 102 + make_row("Revision to build:", 103 "<input type='text' name='revision' />") 104 + '<input type="submit" value="Force Build" /></form>\n')
105
106 -def td(text="", parms={}, **props):
107 data = "" 108 data += " " 109 #if not props.has_key("border"): 110 # props["border"] = 1 111 props.update(parms) 112 comment = props.get("comment", None) 113 if comment: 114 data += "<!-- %s -->" % comment 115 data += "<td" 116 class_ = props.get('class_', None) 117 if class_: 118 props["class"] = class_ 119 for prop in ("align", "colspan", "rowspan", "border", 120 "valign", "halign", "class"): 121 p = props.get(prop, None) 122 if p != None: 123 data += " %s=\"%s\"" % (prop, p) 124 data += ">" 125 if not text: 126 text = "&nbsp;" 127 if isinstance(text, list): 128 data += "<br />".join(text) 129 else: 130 data += text 131 data += "</td>\n" 132 return data
133
134 -def build_get_class(b):
135 """ 136 Return the class to use for a finished build or buildstep, 137 based on the result. 138 """ 139 # FIXME: this getResults duplicity might need to be fixed 140 result = b.getResults() 141 #print "THOMAS: result for b %r: %r" % (b, result) 142 if isinstance(b, builder.BuildStatus): 143 result = b.getResults() 144 elif isinstance(b, builder.BuildStepStatus): 145 result = b.getResults()[0] 146 # after forcing a build, b.getResults() returns ((None, []), []), ugh 147 if isinstance(result, tuple): 148 result = result[0] 149 else: 150 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b 151 152 if result == None: 153 # FIXME: this happens when a buildstep is running ? 154 return "running" 155 return builder.Results[result]
156
157 -def path_to_root(request):
158 # /waterfall : ['waterfall'] -> '' 159 # /somewhere/lower : ['somewhere', 'lower'] -> '../' 160 # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../' 161 # / : [] -> '' 162 if request.prepath: 163 segs = len(request.prepath) - 1 164 else: 165 segs = 0 166 root = "../" * segs 167 return root
168
169 -def path_to_builder(request, builderstatus):
170 return (path_to_root(request) + 171 "builders/" + 172 urllib.quote(builderstatus.getName(), safe=''))
173
174 -def path_to_build(request, buildstatus):
175 return (path_to_builder(request, buildstatus.getBuilder()) + 176 "/builds/%d" % buildstatus.getNumber())
177
178 -def path_to_step(request, stepstatus):
179 return (path_to_build(request, stepstatus.getBuild()) + 180 "/steps/%s" % urllib.quote(stepstatus.getName(), safe=''))
181
182 -def path_to_slave(request, slave):
183 return (path_to_root(request) + 184 "buildslaves/" + 185 urllib.quote(slave.getName(), safe=''))
186
187 -def path_to_change(request, change):
188 return (path_to_root(request) + 189 "changes/%s" % change.number)
190
191 -class Box:
192 # a Box wraps an Event. The Box has HTML <td> parameters that Events 193 # lack, and it has a base URL to which each File's name is relative. 194 # Events don't know about HTML. 195 spacer = False
196 - def __init__(self, text=[], class_=None, urlbase=None, 197 **parms):
198 self.text = text 199 self.class_ = class_ 200 self.urlbase = urlbase 201 self.show_idle = 0 202 if parms.has_key('show_idle'): 203 del parms['show_idle'] 204 self.show_idle = 1 205 206 self.parms = parms
207 # parms is a dict of HTML parameters for the <td> element that will 208 # represent this Event in the waterfall display. 209
210 - def td(self, **props):
211 props.update(self.parms) 212 text = self.text 213 if not text and self.show_idle: 214 text = ["[idle]"] 215 return td(text, props, class_=self.class_)
216 217
218 -class HtmlResource(resource.Resource):
219 # this is a cheap sort of template thingy 220 contentType = "text/html; charset=UTF-8" 221 title = "Buildbot" 222 addSlash = False # adapted from Nevow 223
224 - def getChild(self, path, request):
225 if self.addSlash and path == "" and len(request.postpath) == 0: 226 return self 227 return resource.Resource.getChild(self, path, request)
228
229 - def render(self, request):
230 # tell the WebStatus about the HTTPChannel that got opened, so they 231 # can close it if we get reconfigured and the WebStatus goes away. 232 # They keep a weakref to this, since chances are good that it will be 233 # closed by the browser or by us before we get reconfigured. See 234 # ticket #102 for details. 235 if hasattr(request, "channel"): 236 # web.distrib.Request has no .channel 237 request.site.buildbot_service.registerChannel(request.channel) 238 239 # Our pages no longer require that their URL end in a slash. Instead, 240 # they all use request.childLink() or some equivalent which takes the 241 # last path component into account. This clause is left here for 242 # historical and educational purposes. 243 if False and self.addSlash and request.prepath[-1] != '': 244 # this is intended to behave like request.URLPath().child('') 245 # but we need a relative URL, since we might be living behind a 246 # reverse proxy 247 # 248 # note that the Location: header (as used in redirects) are 249 # required to have absolute URIs, and my attempt to handle 250 # reverse-proxies gracefully violates rfc2616. This frequently 251 # works, but single-component paths sometimes break. The best 252 # strategy is to avoid these redirects whenever possible by using 253 # HREFs with trailing slashes, and only use the redirects for 254 # manually entered URLs. 255 url = request.prePathURL() 256 scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 257 new_url = request.prepath[-1] + "/" 258 if query: 259 new_url += "?" + query 260 request.redirect(new_url) 261 return '' 262 263 data = self.content(request) 264 if isinstance(data, unicode): 265 data = data.encode("utf-8") 266 request.setHeader("content-type", self.contentType) 267 if request.method == "HEAD": 268 request.setHeader("content-length", len(data)) 269 return '' 270 return data
271
272 - def getStatus(self, request):
273 return request.site.buildbot_service.getStatus()
274
275 - def getControl(self, request):
276 return request.site.buildbot_service.getControl()
277
278 - def isUsingUserPasswd(self, request):
279 return request.site.buildbot_service.isUsingUserPasswd()
280
281 - def authUser(self, request):
282 user = request.args.get("username", ["<unknown>"])[0] 283 passwd = request.args.get("passwd", ["<no-password>"])[0] 284 if user == "<unknown>" or passwd == "<no-password>": 285 return False 286 return request.site.buildbot_service.authUser(user, passwd)
287
288 - def getChangemaster(self, request):
289 return request.site.buildbot_service.getChangeSvc()
290
291 - def path_to_root(self, request):
292 return path_to_root(request)
293
294 - def footer(self, s, req):
295 # TODO: this stuff should be generated by a template of some sort 296 projectURL = s.getProjectURL() 297 projectName = s.getProjectName() 298 data = '<hr /><div class="footer">\n' 299 300 welcomeurl = self.path_to_root(req) + "index.html" 301 data += '[<a href="%s">welcome</a>]\n' % welcomeurl 302 data += "<br />\n" 303 304 data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>' 305 data += "-%s " % version 306 if projectName: 307 data += "working for the " 308 if projectURL: 309 data += "<a href=\"%s\">%s</a> project." % (projectURL, 310 projectName) 311 else: 312 data += "%s project." % projectName 313 data += "<br />\n" 314 data += ("Page built: " + 315 time.strftime("%a %d %b %Y %H:%M:%S", 316 time.localtime(util.now())) 317 + "\n") 318 data += '</div>\n' 319 320 return data
321
322 - def getTitle(self, request):
323 return self.title
324
325 - def fillTemplate(self, template, request):
326 s = request.site.buildbot_service 327 values = s.template_values.copy() 328 values['root'] = self.path_to_root(request) 329 # e.g. to reference the top-level 'buildbot.css' page, use 330 # "%(root)sbuildbot.css" 331 values['title'] = self.getTitle(request) 332 return template % values
333
334 - def content(self, request):
335 s = request.site.buildbot_service 336 data = "" 337 data += self.fillTemplate(s.header, request) 338 data += "<head>\n" 339 for he in s.head_elements: 340 data += " " + self.fillTemplate(he, request) + "\n" 341 data += self.head(request) 342 data += "</head>\n\n" 343 344 data += '<body %s>\n' % " ".join(['%s="%s"' % (k,v) 345 for (k,v) in s.body_attrs.items()]) 346 data += self.body(request) 347 data += "</body>\n" 348 data += self.fillTemplate(s.footer, request) 349 return data
350
351 - def head(self, request):
352 return ""
353
354 - def body(self, request):
355 return "Dummy\n"
356
357 -class StaticHTML(HtmlResource):
358 - def __init__(self, body, title):
359 HtmlResource.__init__(self) 360 self.bodyHTML = body 361 self.title = title
362 - def body(self, request):
363 return self.bodyHTML
364 365 MINUTE = 60 366 HOUR = 60*MINUTE 367 DAY = 24*HOUR 368 WEEK = 7*DAY 369 MONTH = 30*DAY 370
371 -def plural(word, words, num):
372 if int(num) == 1: 373 return "%d %s" % (num, word) 374 else: 375 return "%d %s" % (num, words)
376
377 -def abbreviate_age(age):
378 if age <= 90: 379 return "%s ago" % plural("second", "seconds", age) 380 if age < 90*MINUTE: 381 return "about %s ago" % plural("minute", "minutes", age / MINUTE) 382 if age < DAY: 383 return "about %s ago" % plural("hour", "hours", age / HOUR) 384 if age < 2*WEEK: 385 return "about %s ago" % plural("day", "days", age / DAY) 386 if age < 2*MONTH: 387 return "about %s ago" % plural("week", "weeks", age / WEEK) 388 return "a long time ago"
389 390
391 -class OneLineMixin:
392 LINE_TIME_FORMAT = "%b %d %H:%M" 393
394 - def get_line_values(self, req, build):
395 ''' 396 Collect the data needed for each line display 397 ''' 398 builder_name = build.getBuilder().getName() 399 results = build.getResults() 400 text = build.getText() 401 try: 402 rev = build.getProperty("got_revision") 403 if rev is None: 404 rev = "??" 405 except KeyError: 406 rev = "??" 407 rev = str(rev) 408 if len(rev) > 40: 409 rev = "version is too-long" 410 root = self.path_to_root(req) 411 css_class = css_classes.get(results, "") 412 values = {'class': css_class, 413 'builder_name': builder_name, 414 'buildnum': build.getNumber(), 415 'results': css_class, 416 'text': " ".join(build.getText()), 417 'buildurl': path_to_build(req, build), 418 'builderurl': path_to_builder(req, build.getBuilder()), 419 'rev': rev, 420 'time': time.strftime(self.LINE_TIME_FORMAT, 421 time.localtime(build.getTimes()[0])), 422 } 423 return values
424
425 - def make_line(self, req, build, include_builder=True):
426 ''' 427 Format and render a single line into HTML 428 ''' 429 values = self.get_line_values(req, build) 430 fmt_pieces = ['<font size="-1">(%(time)s)</font>', 431 'rev=[%(rev)s]', 432 '<span class="%(class)s">%(results)s</span>', 433 ] 434 if include_builder: 435 fmt_pieces.append('<a href="%(builderurl)s">%(builder_name)s</a>') 436 fmt_pieces.append('<a href="%(buildurl)s">#%(buildnum)d</a>:') 437 fmt_pieces.append('%(text)s') 438 data = " ".join(fmt_pieces) % values 439 return data
440
441 -def map_branches(branches):
442 # when the query args say "trunk", present that to things like 443 # IBuilderStatus.generateFinishedBuilds as None, since that's the 444 # convention in use. But also include 'trunk', because some VC systems 445 # refer to it that way. In the long run we should clean this up better, 446 # maybe with Branch objects or something. 447 if "trunk" in branches: 448 return branches + [None] 449 return branches
450