Clean up display of pending builds with changes
[buildbot.git] / buildbot / status / web / base.py
blob5b835b0a58ed974eb6a01343e64aa22395e5c629
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
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 """
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 """
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 """
29 class IHTMLLog(Interface):
30 pass
32 css_classes = {SUCCESS: "success",
33 WARNINGS: "warnings",
34 FAILURE: "failure",
35 SKIPPED: "skipped",
36 EXCEPTION: "exception",
39 ROW_TEMPLATE = '''
40 <div class="row">
41 <span class="label">%(label)s</span>
42 <span class="field">%(field)s</span>
43 </div>
44 '''
46 def make_row(label, field):
47 """Create a name/value row for the HTML.
49 `label` is plain text; it will be HTML-encoded.
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}
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}."""
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
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
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')
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
134 def build_get_class(b):
136 Return the class to use for a finished build or buildstep,
137 based on the result.
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
152 if result == None:
153 # FIXME: this happens when a buildstep is running ?
154 return "running"
155 return builder.Results[result]
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
169 def path_to_builder(request, builderstatus):
170 return (path_to_root(request) +
171 "builders/" +
172 urllib.quote(builderstatus.getName(), safe=''))
174 def path_to_build(request, buildstatus):
175 return (path_to_builder(request, buildstatus.getBuilder()) +
176 "/builds/%d" % buildstatus.getNumber())
178 def path_to_step(request, stepstatus):
179 return (path_to_build(request, stepstatus.getBuild()) +
180 "/steps/%s" % urllib.quote(stepstatus.getName(), safe=''))
182 def path_to_slave(request, slave):
183 return (path_to_root(request) +
184 "buildslaves/" +
185 urllib.quote(slave.getName(), safe=''))
187 def path_to_change(request, change):
188 return (path_to_root(request) +
189 "changes/%s" % change.number)
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
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.
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_)
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
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)
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)
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
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 ''
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
272 def getStatus(self, request):
273 return request.site.buildbot_service.getStatus()
275 def getControl(self, request):
276 return request.site.buildbot_service.getControl()
278 def isUsingUserPasswd(self, request):
279 return request.site.buildbot_service.isUsingUserPasswd()
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)
288 def getChangemaster(self, request):
289 return request.site.buildbot_service.getChangeSvc()
291 def path_to_root(self, request):
292 return path_to_root(request)
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'
300 welcomeurl = self.path_to_root(req) + "index.html"
301 data += '[<a href="%s">welcome</a>]\n' % welcomeurl
302 data += "<br />\n"
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'
320 return data
322 def getTitle(self, request):
323 return self.title
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
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"
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
351 def head(self, request):
352 return ""
354 def body(self, request):
355 return "Dummy\n"
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
365 MINUTE = 60
366 HOUR = 60*MINUTE
367 DAY = 24*HOUR
368 WEEK = 7*DAY
369 MONTH = 30*DAY
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)
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"
391 class OneLineMixin:
392 LINE_TIME_FORMAT = "%b %d %H:%M"
394 def get_line_values(self, req, build):
396 Collect the data needed for each line display
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])),
423 return values
425 def make_line(self, req, build, include_builder=True):
427 Format and render a single line into HTML
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>',
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
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