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
, EXCEPTION
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",
35 EXCEPTION
: "exception",
40 <span class="label">%(label)s</span>
41 <span class="field">%(field)s</span>
45 def make_row(label
, field
):
46 """Create a name/value row for the HTML.
48 `label` is plain text; it will be HTML-encoded.
50 `field` is a bit of HTML structure; it will not be encoded in
53 label
= html
.escape(label
)
54 return ROW_TEMPLATE
% {"label": label
, "field": field
}
59 def td(text
="", parms
={}, **props
):
62 #if not props.has_key("border"):
65 if props
.has_key("bgcolor"):
66 props
["bgcolor"] = colormap
.get(props
["bgcolor"], props
["bgcolor"])
67 comment
= props
.get("comment", None)
69 data
+= "<!-- %s -->" % comment
71 class_
= props
.get('class_', None)
73 props
["class"] = class_
74 for prop
in ("align", "bgcolor", "colspan", "rowspan", "border",
75 "valign", "halign", "class"):
76 p
= props
.get(prop
, None)
78 data
+= " %s=\"%s\"" % (prop
, p
)
82 if isinstance(text
, list):
83 data
+= "<br />".join(text
)
89 def build_get_class(b
):
91 Return the class to use for a finished build or buildstep,
94 # FIXME: this getResults duplicity might need to be fixed
95 result
= b
.getResults()
96 #print "THOMAS: result for b %r: %r" % (b, result)
97 if isinstance(b
, builder
.BuildStatus
):
98 result
= b
.getResults()
99 elif isinstance(b
, builder
.BuildStepStatus
):
100 result
= b
.getResults()[0]
101 # after forcing a build, b.getResults() returns ((None, []), []), ugh
102 if isinstance(result
, tuple):
105 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
108 # FIXME: this happens when a buildstep is running ?
110 return builder
.Results
[result
]
112 def path_to_root(request
):
113 # /waterfall : ['waterfall'] -> ''
114 # /somewhere/lower : ['somewhere', 'lower'] -> '../'
115 # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../'
118 segs
= len(request
.prepath
) - 1
124 def path_to_builder(request
, builderstatus
):
125 return (path_to_root(request
) +
127 urllib
.quote(builderstatus
.getName(), safe
=''))
129 def path_to_build(request
, buildstatus
):
130 return (path_to_builder(request
, buildstatus
.getBuilder()) +
131 "/builds/%d" % buildstatus
.getNumber())
133 def path_to_step(request
, stepstatus
):
134 return (path_to_build(request
, stepstatus
.getBuild()) +
135 "/steps/%s" % urllib
.quote(stepstatus
.getName(), safe
=''))
138 # a Box wraps an Event. The Box has HTML <td> parameters that Events
139 # lack, and it has a base URL to which each File's name is relative.
140 # Events don't know about HTML.
142 def __init__(self
, text
=[], color
=None, class_
=None, urlbase
=None,
147 self
.urlbase
= urlbase
149 if parms
.has_key('show_idle'):
150 del parms
['show_idle']
154 # parms is a dict of HTML parameters for the <td> element that will
155 # represent this Event in the waterfall display.
157 def td(self
, **props
):
158 props
.update(self
.parms
)
160 if not text
and self
.show_idle
:
162 return td(text
, props
, bgcolor
=self
.color
, class_
=self
.class_
)
165 class HtmlResource(resource
.Resource
):
166 # this is a cheap sort of template thingy
167 contentType
= "text/html; charset=UTF-8"
169 addSlash
= False # adapted from Nevow
171 def getChild(self
, path
, request
):
172 if self
.addSlash
and path
== "" and len(request
.postpath
) == 0:
174 return resource
.Resource
.getChild(self
, path
, request
)
176 def render(self
, request
):
177 # tell the WebStatus about the HTTPChannel that got opened, so they
178 # can close it if we get reconfigured and the WebStatus goes away.
179 # They keep a weakref to this, since chances are good that it will be
180 # closed by the browser or by us before we get reconfigured. See
181 # ticket #102 for details.
182 if hasattr(request
, "channel"):
183 # web.distrib.Request has no .channel
184 request
.site
.buildbot_service
.registerChannel(request
.channel
)
186 # Our pages no longer require that their URL end in a slash. Instead,
187 # they all use request.childLink() or some equivalent which takes the
188 # last path component into account. This clause is left here for
189 # historical and educational purposes.
190 if False and self
.addSlash
and request
.prepath
[-1] != '':
191 # this is intended to behave like request.URLPath().child('')
192 # but we need a relative URL, since we might be living behind a
195 # note that the Location: header (as used in redirects) are
196 # required to have absolute URIs, and my attempt to handle
197 # reverse-proxies gracefully violates rfc2616. This frequently
198 # works, but single-component paths sometimes break. The best
199 # strategy is to avoid these redirects whenever possible by using
200 # HREFs with trailing slashes, and only use the redirects for
201 # manually entered URLs.
202 url
= request
.prePathURL()
203 scheme
, netloc
, path
, query
, fragment
= urlparse
.urlsplit(url
)
204 new_url
= request
.prepath
[-1] + "/"
206 new_url
+= "?" + query
207 request
.redirect(new_url
)
210 data
= self
.content(request
)
211 if isinstance(data
, unicode):
212 data
= data
.encode("utf-8")
213 request
.setHeader("content-type", self
.contentType
)
214 if request
.method
== "HEAD":
215 request
.setHeader("content-length", len(data
))
219 def getStatus(self
, request
):
220 return request
.site
.buildbot_service
.getStatus()
221 def getControl(self
, request
):
222 return request
.site
.buildbot_service
.getControl()
224 def getChangemaster(self
, request
):
225 return request
.site
.buildbot_service
.parent
.change_svc
227 def path_to_root(self
, request
):
228 return path_to_root(request
)
230 def getTitle(self
, request
):
233 def fillTemplate(self
, template
, request
):
234 s
= request
.site
.buildbot_service
235 values
= s
.template_values
.copy()
236 values
['root'] = self
.path_to_root(request
)
237 # e.g. to reference the top-level 'buildbot.css' page, use
238 # "%(root)sbuildbot.css"
239 values
['title'] = self
.getTitle(request
)
240 return template
% values
242 def content(self
, request
):
243 s
= request
.site
.buildbot_service
245 data
+= self
.fillTemplate(s
.header
, request
)
247 for he
in s
.head_elements
:
248 data
+= " " + self
.fillTemplate(he
, request
) + "\n"
249 data
+= self
.head(request
)
250 data
+= "</head>\n\n"
252 data
+= '<body %s>\n' % " ".join(['%s="%s"' % (k
,v
)
253 for (k
,v
) in s
.body_attrs
.items()])
254 data
+= self
.body(request
)
256 data
+= self
.fillTemplate(s
.footer
, request
)
259 def head(self
, request
):
262 def body(self
, request
):
265 class StaticHTML(HtmlResource
):
266 def __init__(self
, body
, title
):
267 HtmlResource
.__init
__(self
)
270 def body(self
, request
):
279 def plural(word
, words
, num
):
281 return "%d %s" % (num
, word
)
283 return "%d %s" % (num
, words
)
285 def abbreviate_age(age
):
287 return "%s ago" % plural("second", "seconds", age
)
289 return "about %s ago" % plural("minute", "minutes", age
/ MINUTE
)
291 return "about %s ago" % plural("hour", "hours", age
/ HOUR
)
293 return "about %s ago" % plural("day", "days", age
/ DAY
)
295 return "about %s ago" % plural("week", "weeks", age
/ WEEK
)
296 return "a long time ago"
300 LINE_TIME_FORMAT
= "%b %d %H:%M"
302 def make_line(self
, req
, build
, include_builder
=True):
303 builder_name
= build
.getBuilder().getName()
304 results
= build
.getResults()
306 rev
= build
.getProperty("got_revision")
312 rev
= "version is too-long"
313 root
= self
.path_to_root(req
)
314 values
= {'class': css_classes
[results
],
315 'builder_name': builder_name
,
316 'buildnum': build
.getNumber(),
317 'results': css_classes
[results
],
319 "builders/%s/builds/%d" % (builder_name
,
321 'builderurl': (root
+ "builders/%s" % builder_name
),
323 'time': time
.strftime(self
.LINE_TIME_FORMAT
,
324 time
.localtime(build
.getTimes()[0])),
327 fmt_pieces
= ['<font size="-1">(%(time)s)</font>',
329 '<span class="%(class)s">%(results)s</span>',
332 fmt_pieces
.append('<a href="%(builderurl)s">%(builder_name)s</a>')
333 fmt_pieces
.append('<a href="%(buildurl)s">#%(buildnum)d</a>:')
334 data
= " ".join(fmt_pieces
) % values
337 def map_branches(branches
):
338 # when the query args say "trunk", present that to things like
339 # IBuilderStatus.generateFinishedBuilds as None, since that's the
340 # convention in use. In the long run we should clean this up better,
341 # maybe with Branch objects or something.
342 return [b
!= "trunk" and b
or None for b
in branches
]