def phase1(self, request, sourceNames, timestamps, eventGrid, sourceEvents): # phase1 rendering: table, but boxes do not overlap data = "" if not timestamps: return data lastDate = None for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert (len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x, y: max(x, y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): data += " <tr>\n" if i == 0: stuff = [] # add the date at the beginning, and each time it changes today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) data += td(stuff, valign="bottom", align="center", rowspan=maxRows, class_="Time") for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert (block != None) # should be [] instead # bottom-justify offset = maxRows - len(block) if i < offset: data += td("") else: e = block[i - offset] box = IBox(e).getBox(request) box.parms["show_idle"] = 1 data += box.td(valign="top", align="center") data += " </tr>\n" return data
def phase1(self, request, sourceNames, timestamps, eventGrid, sourceEvents): # phase1 rendering: table, but boxes do not overlap data = "" if not timestamps: return data lastDate = None for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert(len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x,y: max(x,y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): data += " <tr>\n"; if i == 0: stuff = [] # add the date at the beginning, and each time it changes today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) data += td(stuff, valign="bottom", align="center", rowspan=maxRows, class_="Time") for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert(block != None) # should be [] instead # bottom-justify offset = maxRows - len(block) if i < offset: data += td("") else: e = block[i-offset] box = IBox(e).getBox(request) box.parms["show_idle"] = 1 data += box.td(valign="top", align="center") data += " </tr>\n" return data
def body0(self, request, builders): # build the waterfall display data = "" data += "<h2>Basic display</h2>\n" data += '<p>See <a href="%s">here</a>' % request.childLink( "../waterfall") data += " for the waterfall display</p>\n" data += '<table border="0" cellspacing="0">\n' names = map(lambda builder: builder.name, builders) # the top row is two blank spaces, then the top-level status boxes data += " <tr>\n" data += td("", colspan=2) for b in builders: text = "" color = "#ca88f7" state, builds = b.getState() if state != "offline": text += "%s<br />\n" % state #b.getCurrentBig().text[0] else: text += "OFFLINE<br />\n" color = "#ffe0e0" data += td(text, align="center", bgcolor=color) # the next row has the column headers: time, changes, builder names data += " <tr>\n" data += td("Time", align="center") data += td("Changes", align="center") for name in names: data += td('<a href="%s">%s</a>' % (request.childLink("../" + urllib.quote(name)), name), align="center") data += " </tr>\n" # all further rows involve timestamps, commit events, and build events data += " <tr>\n" data += td("04:00", align="bottom") data += td("fred", align="center") for name in names: data += td("stuff", align="center", bgcolor="red") data += " </tr>\n" data += "</table>\n" return data
def body0(self, request, builders): # build the waterfall display data = "" data += "<h2>Basic display</h2>\n" data += '<p>See <a href="%s">here</a>' % request.childLink("../waterfall") data += " for the waterfall display</p>\n" data += '<table border="0" cellspacing="0">\n' names = map(lambda builder: builder.name, builders) # the top row is two blank spaces, then the top-level status boxes data += " <tr>\n" data += td("", colspan=2) for b in builders: text = "" color = "#ca88f7" state, builds = b.getState() if state != "offline": text += "%s<br />\n" % state #b.getCurrentBig().text[0] else: text += "OFFLINE<br />\n" color = "#ffe0e0" data += td(text, align="center", bgcolor=color) # the next row has the column headers: time, changes, builder names data += " <tr>\n" data += td("Time", align="center") data += td("Changes", align="center") for name in names: data += td('<a href="%s">%s</a>' % (request.childLink("../" + urllib.quote(name)), name), align="center") data += " </tr>\n" # all further rows involve timestamps, commit events, and build events data += " <tr>\n" data += td("04:00", align="bottom") data += td("fred", align="center") for name in names: data += td("stuff", align="center", bgcolor="red") data += " </tr>\n" data += "</table>\n" return data
def phase2(self, request, sourceNames, timestamps, eventGrid, sourceEvents): data = "" if not timestamps: return data # first pass: figure out the height of the chunks, populate grid grid = [] for i in range(1 + len(sourceNames)): grid.append([]) # grid is a list of columns, one for the timestamps, and one per # event source. Each column is exactly the same height. Each element # of the list is a single <td> box. lastDate = time.strftime("<b>%d %b %Y</b>", time.localtime(util.now())) for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert (len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x, y: max(x, y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): if i != maxRows - 1: grid[0].append(None) else: # timestamp goes at the bottom of the chunk stuff = [] # add the date at the beginning (if it is not the same as # today's date), and each time it changes todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) grid[0].append( Box(text=stuff, class_="Time", valign="bottom", align="center")) # at this point the timestamp column has been populated with # maxRows boxes, most None but the last one has the time string for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert (block != None) # should be [] instead for i in range(maxRows - len(block)): # fill top of chunk with blank space grid[c + 1].append(None) for i in range(len(block)): # so the events are bottom-justified b = IBox(block[i]).getBox(request) b.parms['valign'] = "top" b.parms['align'] = "center" grid[c + 1].append(b) # now all the other columns have maxRows new boxes too # populate the last row, if empty gridlen = len(grid[0]) for i in range(len(grid)): strip = grid[i] assert (len(strip) == gridlen) if strip[-1] == None: if sourceEvents[i - 1]: filler = IBox(sourceEvents[i - 1]).getBox(request) else: # this can happen if you delete part of the build history filler = Box(text=["?"], align="center") strip[-1] = filler strip[-1].parms['rowspan'] = 1 # second pass: bubble the events upwards to un-occupied locations # Every square of the grid that has a None in it needs to have # something else take its place. noBubble = request.args.get("nobubble", ['0']) noBubble = int(noBubble[0]) if not noBubble: for col in range(len(grid)): strip = grid[col] if col == 1: # changes are handled differently for i in range(2, len(strip) + 1): # only merge empty boxes. Don't bubble commit boxes. if strip[-i] == None: next = strip[-i + 1] assert (next) if next: #if not next.event: if next.spacer: # bubble the empty box up strip[-i] = next strip[-i].parms['rowspan'] += 1 strip[-i + 1] = None else: # we are above a commit box. Leave it # be, and turn the current box into an # empty one strip[-i] = Box([], rowspan=1, comment="commit bubble") strip[-i].spacer = True else: # we are above another empty box, which # somehow wasn't already converted. # Shouldn't happen pass else: for i in range(2, len(strip) + 1): # strip[-i] will go from next-to-last back to first if strip[-i] == None: # bubble previous item up assert (strip[-i + 1] != None) strip[-i] = strip[-i + 1] strip[-i].parms['rowspan'] += 1 strip[-i + 1] = None else: strip[-i].parms['rowspan'] = 1 # third pass: render the HTML table for i in range(gridlen): data += " <tr>\n" for strip in grid: b = strip[i] if b: data += b.td() else: if noBubble: data += td([]) # Nones are left empty, rowspan should make it all fit data += " </tr>\n" return data
def body(self, request): "This method builds the main waterfall display." status = self.getStatus(request) data = '' projectName = status.getProjectName() projectURL = status.getProjectURL() phase = request.args.get("phase", ["2"]) phase = int(phase[0]) # we start with all Builders available to this Waterfall: this is # limited by the config-file -time categories= argument, and defaults # to all defined Builders. allBuilderNames = status.getBuilderNames(categories=self.categories) builders = [status.getBuilder(name) for name in allBuilderNames] # but if the URL has one or more builder= arguments (or the old show= # argument, which is still accepted for backwards compatibility), we # use that set of builders instead. We still don't show anything # outside the config-file time set limited by categories=. showBuilders = request.args.get("show", []) showBuilders.extend(request.args.get("builder", [])) if showBuilders: builders = [b for b in builders if b.name in showBuilders] # now, if the URL has one or category= arguments, use them as a # filter: only show those builders which belong to one of the given # categories. showCategories = request.args.get("category", []) if showCategories: builders = [b for b in builders if b.category in showCategories] builderNames = [b.name for b in builders] if phase == -1: return self.body0(request, builders) (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \ self.buildGrid(request, builders) if phase == 0: return self.phase0(request, (changeNames + builderNames), timestamps, eventGrid) # start the table: top-header material data += '<table border="0" cellspacing="0">\n' if projectName and projectURL: # TODO: this is going to look really ugly topleft = '<a href="%s">%s</a><br />last build' % \ (projectURL, projectName) else: topleft = "last build" data += ' <tr class="LastBuild">\n' data += td(topleft, align="right", colspan=2, class_="Project") for b in builders: box = ITopBox(b).getBox(request) data += box.td(align="center") data += " </tr>\n" data += ' <tr class="Activity">\n' data += td('current activity', align='right', colspan=2) for b in builders: box = ICurrentBox(b).getBox(status) data += box.td(align="center") data += " </tr>\n" data += " <tr>\n" TZ = time.tzname[time.localtime()[-1]] data += td("time (%s)" % TZ, align="center", class_="Time") data += td('<a href="%s">changes</a>' % request.childLink("../changes"), align="center", class_="Change") for name in builderNames: safename = urllib.quote(name, safe='') data += td('<a href="%s">%s</a>' % (request.childLink("../builders/%s" % safename), name), align="center", class_="Builder") data += " </tr>\n" if phase == 1: f = self.phase1 else: f = self.phase2 data += f(request, changeNames + builderNames, timestamps, eventGrid, sourceEvents) data += "</table>\n" data += '<hr /><div class="footer">\n' def with_args(req, remove_args=[], new_args=[], new_path=None): # sigh, nevow makes this sort of manipulation easier newargs = req.args.copy() for argname in remove_args: newargs[argname] = [] if "branch" in newargs: newargs["branch"] = [b for b in newargs["branch"] if b] for k, v in new_args: if k in newargs: newargs[k].append(v) else: newargs[k] = [v] newquery = "&".join( ["%s=%s" % (k, v) for k in newargs for v in newargs[k]]) if new_path: new_url = new_path elif req.prepath: new_url = req.prepath[-1] else: new_url = '' if newquery: new_url += "?" + newquery return new_url if timestamps: bottom = timestamps[-1] nextpage = with_args(request, ["last_time"], [("last_time", str(int(bottom)))]) data += '[<a href="%s">next page</a>]\n' % nextpage helpurl = self.path_to_root(request) + "waterfall/help" helppage = with_args(request, new_path=helpurl) data += '[<a href="%s">help</a>]\n' % helppage welcomeurl = self.path_to_root(request) + "index.html" data += '[<a href="%s">welcome</a>]\n' % welcomeurl if self.get_reload_time(request) is not None: no_reload_page = with_args(request, remove_args=["reload"]) data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page data += "<br />\n" bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version) data += '<a href="%s">Buildbot-%s</a> ' % (bburl, version) if projectName: data += "working for the " if projectURL: data += '<a href="%s">%s</a> project.' % (projectURL, projectName) else: data += "%s project." % projectName data += "<br />\n" # TODO: push this to the right edge, if possible data += ("Page built: " + time.strftime( "%a %d %b %Y %H:%M:%S", time.localtime(util.now())) + "\n") data += '</div>\n' return "%s %s" % (self.GetAnnounce(), data)
def phase2(self, request, sourceNames, timestamps, eventGrid, sourceEvents): data = "" if not timestamps: return data # first pass: figure out the height of the chunks, populate grid grid = [] for i in range(1+len(sourceNames)): grid.append([]) # grid is a list of columns, one for the timestamps, and one per # event source. Each column is exactly the same height. Each element # of the list is a single <td> box. lastDate = time.strftime("<b>%d %b %Y</b>", time.localtime(util.now())) for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert(len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x,y: max(x,y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): if i != maxRows-1: grid[0].append(None) else: # timestamp goes at the bottom of the chunk stuff = [] # add the date at the beginning (if it is not the same as # today's date), and each time it changes todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) grid[0].append(Box(text=stuff, class_="Time", valign="bottom", align="center")) # at this point the timestamp column has been populated with # maxRows boxes, most None but the last one has the time string for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert(block != None) # should be [] instead for i in range(maxRows - len(block)): # fill top of chunk with blank space grid[c+1].append(None) for i in range(len(block)): # so the events are bottom-justified b = IBox(block[i]).getBox(request) b.parms['valign'] = "top" b.parms['align'] = "center" grid[c+1].append(b) # now all the other columns have maxRows new boxes too # populate the last row, if empty gridlen = len(grid[0]) for i in range(len(grid)): strip = grid[i] assert(len(strip) == gridlen) if strip[-1] == None: if sourceEvents[i-1]: filler = IBox(sourceEvents[i-1]).getBox(request) else: # this can happen if you delete part of the build history filler = Box(text=["?"], align="center") strip[-1] = filler strip[-1].parms['rowspan'] = 1 # second pass: bubble the events upwards to un-occupied locations # Every square of the grid that has a None in it needs to have # something else take its place. noBubble = request.args.get("nobubble",['0']) noBubble = int(noBubble[0]) if not noBubble: for col in range(len(grid)): strip = grid[col] if col == 1: # changes are handled differently for i in range(2, len(strip)+1): # only merge empty boxes. Don't bubble commit boxes. if strip[-i] == None: next = strip[-i+1] assert(next) if next: #if not next.event: if next.spacer: # bubble the empty box up strip[-i] = next strip[-i].parms['rowspan'] += 1 strip[-i+1] = None else: # we are above a commit box. Leave it # be, and turn the current box into an # empty one strip[-i] = Box([], rowspan=1, comment="commit bubble") strip[-i].spacer = True else: # we are above another empty box, which # somehow wasn't already converted. # Shouldn't happen pass else: for i in range(2, len(strip)+1): # strip[-i] will go from next-to-last back to first if strip[-i] == None: # bubble previous item up assert(strip[-i+1] != None) strip[-i] = strip[-i+1] strip[-i].parms['rowspan'] += 1 strip[-i+1] = None else: strip[-i].parms['rowspan'] = 1 # third pass: render the HTML table for i in range(gridlen): data += " <tr>\n"; for strip in grid: b = strip[i] if b: # convert data to a unicode string, whacking any non-ASCII characters it might contain s = b.td() if isinstance(s, unicode): s = s.encode("utf-8", "replace") data += s else: if noBubble: data += td([]) # Nones are left empty, rowspan should make it all fit data += " </tr>\n" return data
def body(self, request): "This method builds the main waterfall display." status = self.getStatus(request) data = '' projectName = status.getProjectName() projectURL = status.getProjectURL() phase = request.args.get("phase",["2"]) phase = int(phase[0]) # we start with all Builders available to this Waterfall: this is # limited by the config-file -time categories= argument, and defaults # to all defined Builders. allBuilderNames = status.getBuilderNames(categories=self.categories) builders = [status.getBuilder(name) for name in allBuilderNames] # but if the URL has one or more builder= arguments (or the old show= # argument, which is still accepted for backwards compatibility), we # use that set of builders instead. We still don't show anything # outside the config-file time set limited by categories=. showBuilders = request.args.get("show", []) showBuilders.extend(request.args.get("builder", [])) if showBuilders: builders = [b for b in builders if b.name in showBuilders] # now, if the URL has one or category= arguments, use them as a # filter: only show those builders which belong to one of the given # categories. showCategories = request.args.get("category", []) if showCategories: builders = [b for b in builders if b.category in showCategories] # If the URL has the failures_only=true argument, we remove all the # builders that are not currently red or won't be turning red at the end # of their current run. failuresOnly = request.args.get("failures_only", ["false"])[0] if failuresOnly.lower() == "true": builders = [b for b in builders if not self.isSuccess(b)] builderNames = [b.name for b in builders] if phase == -1: return self.body0(request, builders) (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \ self.buildGrid(request, builders) if phase == 0: return self.phase0(request, (changeNames + builderNames), timestamps, eventGrid) # start the table: top-header material data += '<table border="0" cellspacing="0">\n' if projectName and projectURL: # TODO: this is going to look really ugly topleft = '<a href="%s">%s</a><br />last build' % \ (projectURL, projectName) else: topleft = "last build" data += ' <tr class="LastBuild">\n' data += td(topleft, align="right", colspan=2, class_="Project") for b in builders: box = ITopBox(b).getBox(request) data += box.td(align="center") data += " </tr>\n" data += ' <tr class="Activity">\n' data += td('current activity', align='right', colspan=2) for b in builders: box = ICurrentBox(b).getBox(status) data += box.td(align="center") data += " </tr>\n" data += " <tr>\n" TZ = time.tzname[time.localtime()[-1]] data += td("time (%s)" % TZ, align="center", class_="Time") data += td('<a href="%s">changes</a>' % request.childLink("../changes"), align="center", class_="Change") for name in builderNames: safename = urllib.quote(name, safe='') data += td('<a href="%s">%s</a>' % (request.childLink("../builders/%s" % safename), name), align="center", class_="Builder") data += " </tr>\n" if phase == 1: f = self.phase1 else: f = self.phase2 data += f(request, changeNames + builderNames, timestamps, eventGrid, sourceEvents) data += "</table>\n" data += '<hr /><div class="footer">\n' def with_args(req, remove_args=[], new_args=[], new_path=None): # sigh, nevow makes this sort of manipulation easier newargs = req.args.copy() for argname in remove_args: newargs[argname] = [] if "branch" in newargs: newargs["branch"] = [b for b in newargs["branch"] if b] for k,v in new_args: if k in newargs: newargs[k].append(v) else: newargs[k] = [v] newquery = "&".join(["%s=%s" % (k, v) for k in newargs for v in newargs[k] ]) if new_path: new_url = new_path elif req.prepath: new_url = req.prepath[-1] else: new_url = '' if newquery: new_url += "?" + newquery return new_url if timestamps: bottom = timestamps[-1] nextpage = with_args(request, ["last_time"], [("last_time", str(int(bottom)))]) data += '[<a href="%s">next page</a>]\n' % nextpage helpurl = self.path_to_root(request) + "waterfall/help" helppage = with_args(request, new_path=helpurl) data += '[<a href="%s">help</a>]\n' % helppage welcomeurl = self.path_to_root(request) + "index.html" data += '[<a href="%s">welcome</a>]\n' % welcomeurl if self.get_reload_time(request) is not None: no_reload_page = with_args(request, remove_args=["reload"]) data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page data += "<br />\n" bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version) data += '<a href="%s">Buildbot-%s</a> ' % (bburl, version) if projectName: data += "working for the " if projectURL: data += '<a href="%s">%s</a> project.' % (projectURL, projectName) else: data += "%s project." % projectName data += "<br />\n" # TODO: push this to the right edge, if possible data += ("Page built: " + time.strftime("%a %d %b %Y %H:%M:%S", time.localtime(util.now())) + "\n") data += '</div>\n' return data