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

Source Code for Module buildbot.status.web.base

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