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.
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.
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>
29 class IHTMLLog(Interface
):
32 css_classes
= {SUCCESS
: "success",
36 EXCEPTION
: "exception",
41 <span class="label">%(label)s</span>
42 <span class="field">%(field)s</span>
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
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
63 label
= "Your username:"
66 data
= make_row(label
, '<input type="text" name="username" />')
68 data
+= make_row("Your password:",
69 '<input type="password" name="passwd" />')
72 def make_stop_form(stopURL
, useUserPasswd
, on_all
=False, label
="Build"):
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
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
87 def make_force_build_form(forceURL
, useUserPasswd
, on_all
=False):
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
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
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
):
109 #if not props.has_key("border"):
110 # props["border"] = 1
112 comment
= props
.get("comment", None)
114 data
+= "<!-- %s -->" % comment
116 class_
= props
.get('class_', None)
118 props
["class"] = class_
119 for prop
in ("align", "colspan", "rowspan", "border",
120 "valign", "halign", "class"):
121 p
= props
.get(prop
, None)
123 data
+= " %s=\"%s\"" % (prop
, p
)
127 if isinstance(text
, list):
128 data
+= "<br />".join(text
)
134 def build_get_class(b
):
136 Return the class to use for a finished build or buildstep,
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):
150 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
153 # FIXME: this happens when a buildstep is 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', ''] -> '../../'
163 segs
= len(request
.prepath
) - 1
169 def path_to_builder(request
, builderstatus
):
170 return (path_to_root(request
) +
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
) +
185 urllib
.quote(slave
.getName(), safe
=''))
187 def path_to_change(request
, change
):
188 return (path_to_root(request
) +
189 "changes/%s" % change
.number
)
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.
196 def __init__(self
, text
=[], class_
=None, urlbase
=None,
200 self
.urlbase
= urlbase
202 if parms
.has_key('show_idle'):
203 del parms
['show_idle']
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
)
213 if not text
and self
.show_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"
222 addSlash
= False # adapted from Nevow
224 def getChild(self
, path
, request
):
225 if self
.addSlash
and path
== "" and len(request
.postpath
) == 0:
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
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] + "/"
259 new_url
+= "?" + query
260 request
.redirect(new_url
)
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
))
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>":
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
304 data
+= '<a href="http://buildbot.sourceforge.net/">Buildbot</a>'
305 data
+= "-%s " % version
307 data
+= "working for the "
309 data
+= "<a href=\"%s\">%s</a> project." % (projectURL
,
312 data
+= "%s project." % projectName
314 data
+= ("Page built: " +
315 time
.strftime("%a %d %b %Y %H:%M:%S",
316 time
.localtime(util
.now()))
322 def getTitle(self
, request
):
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
337 data
+= self
.fillTemplate(s
.header
, request
)
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
)
348 data
+= self
.fillTemplate(s
.footer
, request
)
351 def head(self
, request
):
354 def body(self
, request
):
357 class StaticHTML(HtmlResource
):
358 def __init__(self
, body
, title
):
359 HtmlResource
.__init
__(self
)
362 def body(self
, request
):
371 def plural(word
, words
, num
):
373 return "%d %s" % (num
, word
)
375 return "%d %s" % (num
, words
)
377 def abbreviate_age(age
):
379 return "%s ago" % plural("second", "seconds", age
)
381 return "about %s ago" % plural("minute", "minutes", age
/ MINUTE
)
383 return "about %s ago" % plural("hour", "hours", age
/ HOUR
)
385 return "about %s ago" % plural("day", "days", age
/ DAY
)
387 return "about %s ago" % plural("week", "weeks", age
/ WEEK
)
388 return "a long time ago"
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()
402 rev
= build
.getProperty("got_revision")
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()),
420 'time': time
.strftime(self
.LINE_TIME_FORMAT
,
421 time
.localtime(build
.getTimes()[0])),
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>',
432 '<span class="%(class)s">%(results)s</span>',
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
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]