web: handle branch=trunk usefully
[buildbot.git] / buildbot / status / web / base.py
blob2fcc12be051ff635347bc0c3ab3eab8b0bdc44ea
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.
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 EXCEPTION: "exception",
38 ROW_TEMPLATE = '''
39 <div class="row">
40 <span class="label">%(label)s</span>
41 <span class="field">%(field)s</span>
42 </div>
43 '''
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
51 any way.
52 """
53 label = html.escape(label)
54 return ROW_TEMPLATE % {"label": label, "field": field}
56 colormap = {
57 'green': '#72ff75',
59 def td(text="", parms={}, **props):
60 data = ""
61 data += " "
62 #if not props.has_key("border"):
63 # props["border"] = 1
64 props.update(parms)
65 if props.has_key("bgcolor"):
66 props["bgcolor"] = colormap.get(props["bgcolor"], props["bgcolor"])
67 comment = props.get("comment", None)
68 if comment:
69 data += "<!-- %s -->" % comment
70 data += "<td"
71 class_ = props.get('class_', None)
72 if class_:
73 props["class"] = class_
74 for prop in ("align", "bgcolor", "colspan", "rowspan", "border",
75 "valign", "halign", "class"):
76 p = props.get(prop, None)
77 if p != None:
78 data += " %s=\"%s\"" % (prop, p)
79 data += ">"
80 if not text:
81 text = "&nbsp;"
82 if isinstance(text, list):
83 data += "<br />".join(text)
84 else:
85 data += text
86 data += "</td>\n"
87 return data
89 def build_get_class(b):
90 """
91 Return the class to use for a finished build or buildstep,
92 based on the result.
93 """
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):
103 result = result[0]
104 else:
105 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
107 if result == None:
108 # FIXME: this happens when a buildstep is running ?
109 return "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', ''] -> '../../'
116 # / : [] -> ''
117 if request.prepath:
118 segs = len(request.prepath) - 1
119 else:
120 segs = 0
121 root = "../" * segs
122 return root
124 def path_to_builder(request, builderstatus):
125 return (path_to_root(request) +
126 "builders/" +
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=''))
137 class Box:
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.
141 spacer = False
142 def __init__(self, text=[], color=None, class_=None, urlbase=None,
143 **parms):
144 self.text = text
145 self.color = color
146 self.class_ = class_
147 self.urlbase = urlbase
148 self.show_idle = 0
149 if parms.has_key('show_idle'):
150 del parms['show_idle']
151 self.show_idle = 1
153 self.parms = parms
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)
159 text = self.text
160 if not text and self.show_idle:
161 text = ["[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"
168 title = "Buildbot"
169 addSlash = False # adapted from Nevow
171 def getChild(self, path, request):
172 if self.addSlash and path == "" and len(request.postpath) == 0:
173 return self
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
193 # reverse proxy
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] + "/"
205 if query:
206 new_url += "?" + query
207 request.redirect(new_url)
208 return ''
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))
216 return ''
217 return 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):
231 return self.title
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
244 data = ""
245 data += self.fillTemplate(s.header, request)
246 data += "<head>\n"
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)
255 data += "</body>\n"
256 data += self.fillTemplate(s.footer, request)
257 return data
259 def head(self, request):
260 return ""
262 def body(self, request):
263 return "Dummy\n"
265 class StaticHTML(HtmlResource):
266 def __init__(self, body, title):
267 HtmlResource.__init__(self)
268 self.bodyHTML = body
269 self.title = title
270 def body(self, request):
271 return self.bodyHTML
273 MINUTE = 60
274 HOUR = 60*MINUTE
275 DAY = 24*HOUR
276 WEEK = 7*DAY
277 MONTH = 30*DAY
279 def plural(word, words, num):
280 if int(num) == 1:
281 return "%d %s" % (num, word)
282 else:
283 return "%d %s" % (num, words)
285 def abbreviate_age(age):
286 if age <= 90:
287 return "%s ago" % plural("second", "seconds", age)
288 if age < 90*MINUTE:
289 return "about %s ago" % plural("minute", "minutes", age / MINUTE)
290 if age < DAY:
291 return "about %s ago" % plural("hour", "hours", age / HOUR)
292 if age < 2*WEEK:
293 return "about %s ago" % plural("day", "days", age / DAY)
294 if age < 2*MONTH:
295 return "about %s ago" % plural("week", "weeks", age / WEEK)
296 return "a long time ago"
299 class OneLineMixin:
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()
305 try:
306 rev = build.getProperty("got_revision")
307 if rev is None:
308 rev = "??"
309 except KeyError:
310 rev = "??"
311 if len(rev) > 20:
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],
318 'buildurl': (root +
319 "builders/%s/builds/%d" % (builder_name,
320 build.getNumber())),
321 'builderurl': (root + "builders/%s" % builder_name),
322 'rev': rev,
323 'time': time.strftime(self.LINE_TIME_FORMAT,
324 time.localtime(build.getTimes()[0])),
327 fmt_pieces = ['<font size="-1">(%(time)s)</font>',
328 'rev=[%(rev)s]',
329 '<span class="%(class)s">%(results)s</span>',
331 if include_builder:
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
335 return data
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]