1 # -*- test-case-name: buildbot.test.test_web -*-
3 from zope
.interface
import implements
4 from twisted
.python
import log
, components
5 from twisted
.web
import html
10 from buildbot
import interfaces
, util
11 from buildbot
import version
12 from buildbot
.status
import builder
14 from buildbot
.status
.web
.base
import Box
, HtmlResource
, IBox
, ICurrentBox
, \
15 ITopBox
, td
, build_get_class
19 class CurrentBox(components
.Adapter
):
20 # this provides the "current activity" box, just above the builder name
21 implements(ICurrentBox
)
23 def formatETA(self
, eta
):
28 abstime
= time
.strftime("%H:%M:%S", time
.localtime(util
.now()+eta
))
29 return ["ETA in", "%d secs" % eta
, "at %s" % abstime
]
31 def getBox(self
, status
):
32 # getState() returns offline, idle, or building
33 state
, builds
= self
.original
.getState()
35 # look for upcoming builds. We say the state is "waiting" if the
36 # builder is otherwise idle and there is a scheduler which tells us a
37 # build will be performed some time in the near future. TODO: this
38 # functionality used to be in BuilderStatus.. maybe this code should
39 # be merged back into it.
41 builderName
= self
.original
.getName()
42 for s
in status
.getSchedulers():
43 if builderName
in s
.listBuilderNames():
44 upcoming
.extend(s
.getPendingBuildTimes())
45 if state
== "idle" and upcoming
:
48 if state
== "building":
55 text
.extend(self
.formatETA(eta
))
56 elif state
== "offline":
62 elif state
== "waiting":
66 # just in case I add a state and forget to update this
70 # TODO: for now, this pending/upcoming stuff is in the "current
71 # activity" box, but really it should go into a "next activity" row
72 # instead. The only times it should show up in "current activity" is
73 # when the builder is otherwise idle.
75 # are any builds pending? (waiting for a slave to be free)
76 pbs
= self
.original
.getPendingBuilds()
78 text
.append("%d pending" % len(pbs
))
80 text
.extend(["next at",
81 time
.strftime("%H:%M:%S", time
.localtime(t
)),
82 "[%d secs]" % (t
- util
.now()),
84 # TODO: the upcoming-builds box looks like:
85 # ['waiting', 'next at', '22:14:15', '[86 secs]']
86 # while the currently-building box is reversed:
87 # ['building', 'ETA in', '2 secs', 'at 22:12:50']
88 # consider swapping one of these to make them look the same. also
89 # consider leaving them reversed to make them look different.
90 return Box(text
, color
=color
, class_
="Activity " + state
)
92 components
.registerAdapter(CurrentBox
, builder
.BuilderStatus
, ICurrentBox
)
95 class BuildTopBox(components
.Adapter
):
96 # this provides a per-builder box at the very top of the display,
97 # showing the results of the most recent build
101 assert interfaces
.IBuilderStatus(self
.original
)
102 b
= self
.original
.getLastFinishedBuild()
104 return Box(["none"], "white", class_
="LastBuild")
105 name
= b
.getBuilder().getName()
106 number
= b
.getNumber()
107 url
= "%s/builds/%d" % (name
, number
)
110 # TODO: add link to the per-build page at 'url'
112 class_
= build_get_class(b
)
113 return Box(text
, c
, class_
="LastBuild %s" % class_
)
114 components
.registerAdapter(BuildTopBox
, builder
.BuilderStatus
, ITopBox
)
116 class BuildBox(components
.Adapter
):
117 # this provides the yellow "starting line" box for each build
122 name
= b
.getBuilder().getName()
123 number
= b
.getNumber()
124 url
= "builders/%s/builds/%d" % (urllib
.quote(name
, safe
=''), number
)
125 reason
= b
.getReason()
126 text
= ('<a title="Reason: %s" href="%s">Build %d</a>'
127 % (html
.escape(reason
), url
, number
))
130 if b
.isFinished() and not b
.getSteps():
131 # the steps have been pruned, so there won't be any indication
132 # of whether it succeeded or failed. Color the box red or green
135 class_
= build_get_class(b
)
136 return Box([text
], color
=color
, class_
="BuildStep " + class_
)
137 components
.registerAdapter(BuildBox
, builder
.BuildStatus
, IBox
)
139 class StepBox(components
.Adapter
):
143 b
= self
.original
.getBuild()
144 urlbase
= "builders/%s/builds/%d/steps/%s" % (
145 urllib
.quote(b
.getBuilder().getName(), safe
=''),
147 urllib
.quote(self
.original
.getName(), safe
=''))
148 text
= self
.original
.getText()
150 log
.msg("getText() gave None", urlbase
)
153 logs
= self
.original
.getLogs()
154 for num
in range(len(logs
)):
155 name
= logs
[num
].getName()
156 if logs
[num
].hasContents():
157 url
= urlbase
+ "/logs/%s" % urllib
.quote(name
)
158 text
.append("<a href=\"%s\">%s</a>" % (url
, html
.escape(name
)))
160 text
.append(html
.escape(name
))
161 urls
= self
.original
.getURLs()
162 ex_url_class
= "BuildStep external"
163 for name
, target
in urls
.items():
164 text
.append('[<a href="%s" class="%s">%s</a>]' %
165 (target
, ex_url_class
, html
.escape(name
)))
166 color
= self
.original
.getColor()
167 class_
= "BuildStep " + build_get_class(self
.original
)
168 return Box(text
, color
, class_
=class_
)
169 components
.registerAdapter(StepBox
, builder
.BuildStepStatus
, IBox
)
172 class EventBox(components
.Adapter
):
176 text
= self
.original
.getText()
177 color
= self
.original
.getColor()
180 class_
+= " " + color
181 return Box(text
, color
, class_
=class_
)
182 components
.registerAdapter(EventBox
, builder
.Event
, IBox
)
185 class Spacer(builder
.Event
):
186 def __init__(self
, start
, finish
):
188 self
.finished
= finish
190 class SpacerBox(components
.Adapter
):
194 #b = Box(["spacer"], "white")
198 components
.registerAdapter(SpacerBox
, Spacer
, IBox
)
200 def insertGaps(g
, lastEventTime
, idleGap
=2):
204 starts
, finishes
= e
.getTimes()
205 if debug
: log
.msg("E0", starts
, finishes
)
208 if debug
: log
.msg("E1 finishes=%s, gap=%s, lET=%s" % \
209 (finishes
, idleGap
, lastEventTime
))
210 if finishes
is not None and finishes
+ idleGap
< lastEventTime
:
211 if debug
: log
.msg(" spacer0")
212 yield Spacer(finishes
, lastEventTime
)
214 followingEventStarts
= starts
215 if debug
: log
.msg(" fES0", starts
)
220 starts
, finishes
= e
.getTimes()
221 if debug
: log
.msg("E2", starts
, finishes
)
224 if finishes
is not None and finishes
+ idleGap
< followingEventStarts
:
225 # there is a gap between the end of this event and the beginning
226 # of the next one. Insert an idle event so the waterfall display
229 log
.msg(" finishes=%s, gap=%s, fES=%s" % \
230 (finishes
, idleGap
, followingEventStarts
))
231 yield Spacer(finishes
, followingEventStarts
)
233 followingEventStarts
= starts
234 if debug
: log
.msg(" fES1", starts
)
237 <form action="../waterfall" method="GET">
239 <h1>The Waterfall Display</h1>
241 <p>The Waterfall display can be controlled by adding query arguments to the
242 URL. For example, if your Waterfall is accessed via the URL
243 <tt>http://buildbot.example.org:8080</tt>, then you could add a
244 <tt>branch=</tt> argument (described below) by going to
245 <tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that
246 query arguments are separated from each other with ampersands, but they are
247 separated from the main URL with a question mark, so to add a
248 <tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use
249 <tt>http://buildbot.example.org:8080?branch=beta4&builder=unix&builder=macos</tt>.</p>
251 <h2>Limiting the Displayed Interval</h2>
253 <p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the
254 start of 1970) that will be used as an upper bound on the interval of events
255 displayed: nothing will be shown that is more recent than the given time.
256 When no argument is provided, all events up to and including the most recent
257 steps are included.</p>
259 <p>The <tt>first_time=</tt> argument provides the lower bound. No events will
260 be displayed that occurred <b>before</b> this timestamp. Instead of providing
261 <tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case,
262 <tt>first_time</tt> will be set equal to <tt>last_time</tt> minus
263 <tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p>
265 <p>The display normally shows the latest 200 events that occurred in the
266 given interval, where each timestamp on the left hand edge counts as a single
267 event. You can add a <tt>num_events=</tt> argument to override this this.</p>
269 <h2>Hiding non-Build events</h2>
271 <p>By passing <tt>show_events=false</tt>, you can remove the "buildslave
272 attached", "buildslave detached", and "builder reconfigured" events that
273 appear in-between the actual builds.</p>
275 %(show_events_input)s
277 <h2>Showing only Certain Branches</h2>
279 <p>If you provide one or more <tt>branch=</tt> arguments, the display will be
280 limited to builds that used one of the given branches. If no <tt>branch=</tt>
281 arguments are given, builds from all branches will be displayed.</p>
283 Erase the text from these "Show Branch:" boxes to remove that branch filter.
285 %(show_branches_input)s
287 <h2>Limiting the Builders that are Displayed</h2>
289 <p>By adding one or more <tt>builder=</tt> arguments, the display will be
290 limited to showing builds that ran on the given builders. This serves to
291 limit the display to the specific named columns. If no <tt>builder=</tt>
292 arguments are provided, all Builders will be displayed.</p>
294 <p>To view a Waterfall page with only a subset of Builders displayed, select
295 the Builders you are interested in here.</p>
297 %(show_builders_input)s
300 <h2>Auto-reloading the Page</h2>
302 <p>Adding a <tt>reload=</tt> argument will cause the page to automatically
303 reload itself after that many seconds.</p>
305 %(show_reload_input)s
308 <input type="submit" value="View Waterfall" />
312 class WaterfallHelp(HtmlResource
):
313 title
= "Waterfall Help"
315 def __init__(self
, categories
=None):
316 HtmlResource
.__init
__(self
)
317 self
.categories
= categories
319 def body(self
, request
):
321 status
= self
.getStatus(request
)
323 showEvents_checked
= 'checked="checked"'
324 if request
.args
.get("show_events", ["true"])[0].lower() == "true":
325 showEvents_checked
= ''
326 show_events_input
= ('<p>'
327 '<input type="checkbox" name="show_events" '
329 'Hide non-Build events'
331 ) % showEvents_checked
334 for b
in request
.args
.get("branch", [])
337 show_branches_input
= '<table>\n'
339 show_branches_input
+= ('<tr>'
341 '<input type="text" name="branch" '
345 show_branches_input
+= '</table>\n'
347 # this has a set of toggle-buttons to let the user choose the
349 showBuilders
= request
.args
.get("show", [])
350 showBuilders
.extend(request
.args
.get("builder", []))
351 allBuilders
= status
.getBuilderNames(categories
=self
.categories
)
353 show_builders_input
= '<table>\n'
354 for bn
in allBuilders
:
356 if bn
in showBuilders
:
357 checked
= 'checked="checked"'
358 show_builders_input
+= ('<tr>'
359 '<td><input type="checkbox"'
361 'value="%s" %s></td> '
363 ) % (bn
, checked
, bn
)
364 show_builders_input
+= '</table>\n'
366 # a couple of radio-button selectors for refresh time will appear
367 # just after that text
368 show_reload_input
= '<table>\n'
369 times
= [("none", "None"),
370 ("60", "60 seconds"),
371 ("300", "5 minutes"),
372 ("600", "10 minutes"),
374 current_reload_time
= request
.args
.get("reload", ["none"])
375 if current_reload_time
:
376 current_reload_time
= current_reload_time
[0]
377 if current_reload_time
not in [t
[0] for t
in times
]:
378 times
.insert(0, (current_reload_time
, current_reload_time
) )
379 for value
, name
in times
:
381 if value
== current_reload_time
:
382 checked
= 'checked="checked"'
383 show_reload_input
+= ('<tr>'
384 '<td><input type="radio" name="reload" '
385 'value="%s" %s></td> '
387 ) % (value
, checked
, name
)
388 show_reload_input
+= '</table>\n'
390 fields
= {"show_events_input": show_events_input
,
391 "show_branches_input": show_branches_input
,
392 "show_builders_input": show_builders_input
,
393 "show_reload_input": show_reload_input
,
395 data
+= HELP
% fields
398 class WaterfallStatusResource(HtmlResource
):
399 """This builds the main status page, with the waterfall display, and
402 def __init__(self
, categories
=None):
403 HtmlResource
.__init
__(self
)
404 self
.categories
= categories
405 self
.putChild("help", WaterfallHelp(categories
))
407 def getTitle(self
, request
):
408 status
= self
.getStatus(request
)
409 p
= status
.getProjectName()
411 return "BuildBot: %s" % p
415 def getChangemaster(self
, request
):
416 # TODO: this wants to go away, access it through IStatus
417 return request
.site
.buildbot_service
.parent
.change_svc
419 def get_reload_time(self
, request
):
420 if "reload" in request
.args
:
422 reload_time
= int(request
.args
["reload"][0])
429 def head(self
, request
):
431 reload_time
= self
.get_reload_time(request
)
432 if reload_time
is not None:
433 head
+= '<meta http-equiv="refresh" content="%d">\n' % reload_time
436 def body(self
, request
):
437 "This method builds the main waterfall display."
439 status
= self
.getStatus(request
)
442 projectName
= status
.getProjectName()
443 projectURL
= status
.getProjectURL()
445 phase
= request
.args
.get("phase",["2"])
446 phase
= int(phase
[0])
448 showBuilders
= request
.args
.get("show", [])
449 showBuilders
.extend(request
.args
.get("builder", []))
450 allBuilders
= status
.getBuilderNames(categories
=self
.categories
)
453 for b
in showBuilders
:
454 if b
not in allBuilders
:
456 if b
in builderNames
:
458 builderNames
.append(b
)
460 builderNames
= allBuilders
461 builders
= map(lambda name
: status
.getBuilder(name
),
465 return self
.body0(request
, builders
)
466 (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
) = \
467 self
.buildGrid(request
, builders
)
469 return self
.phase0(request
, (changeNames
+ builderNames
),
470 timestamps
, eventGrid
)
471 # start the table: top-header material
472 data
+= '<table border="0" cellspacing="0">\n'
474 if projectName
and projectURL
:
475 # TODO: this is going to look really ugly
476 topleft
= "<a href=\"%s\">%s</a><br />last build" % \
477 (projectURL
, projectName
)
479 topleft
= "last build"
480 data
+= ' <tr class="LastBuild">\n'
481 data
+= td(topleft
, align
="right", colspan
=2, class_
="Project")
483 box
= ITopBox(b
).getBox()
484 data
+= box
.td(align
="center")
487 data
+= ' <tr class="Activity">\n'
488 data
+= td('current activity', align
='right', colspan
=2)
490 box
= ICurrentBox(b
).getBox(status
)
491 data
+= box
.td(align
="center")
495 TZ
= time
.tzname
[time
.daylight
]
496 data
+= td("time (%s)" % TZ
, align
="center", class_
="Time")
497 name
= changeNames
[0]
499 "<a href=\"%s\">%s</a>" % (urllib
.quote(name
, safe
=''), name
),
500 align
="center", class_
="Change")
501 for name
in builderNames
:
502 safename
= urllib
.quote(name
, safe
='')
503 data
+= td( "<a href=\"builders/%s\">%s</a>" % (safename
, name
),
504 align
="center", class_
="Builder")
511 data
+= f(request
, changeNames
+ builderNames
, timestamps
, eventGrid
,
518 def with_args(req
, remove_args
=[], new_args
=[], new_path
=None):
519 # sigh, nevow makes this sort of manipulation easier
520 newargs
= req
.args
.copy()
521 for argname
in remove_args
:
522 newargs
[argname
] = []
523 newargs
["branch"] = [b
for b
in newargs
["branch"] if b
]
529 newquery
= "&".join(["%s=%s" % (k
, v
)
533 new_url
= req
.URLPath()
535 new_url
.path
= new_path
536 new_url
.query
= newquery
538 # new_url += "?" + newquery
542 bottom
= timestamps
[-1]
543 nextpage
= with_args(request
, ["last_time"],
544 [("last_time", str(int(bottom
)))])
545 data
+= '<a href="%s">next page</a>\n' % nextpage
547 helppage
= with_args(request
, new_path
="waterfall/help")
548 data
+= '<a href="%s">help</a>\n' % helppage
550 if self
.get_reload_time(request
) is not None:
551 no_reload_page
= with_args(request
, remove_args
=["reload"])
552 data
+= '<a href="%s">Stop Reloading</a>\n' % no_reload_page
557 bburl
= "http://buildbot.net/?bb-ver=%s" % urllib
.quote(version
)
558 data
+= "<a href=\"%s\">Buildbot-%s</a> " % (bburl
, version
)
560 data
+= "working for the "
562 data
+= "<a href=\"%s\">%s</a> project." % (projectURL
,
565 data
+= "%s project." % projectName
567 # TODO: push this to the right edge, if possible
568 data
+= ("Page built: " +
569 time
.strftime("%a %d %b %Y %H:%M:%S",
570 time
.localtime(util
.now()))
574 def body0(self
, request
, builders
):
575 # build the waterfall display
577 data
+= "<h2>Basic display</h2>\n"
578 data
+= "<p>See <a href=\"%s\">here</a>" % \
579 urllib
.quote(request
.childLink("waterfall"))
580 data
+= " for the waterfall display</p>\n"
582 data
+= '<table border="0" cellspacing="0">\n'
583 names
= map(lambda builder
: builder
.name
, builders
)
585 # the top row is two blank spaces, then the top-level status boxes
587 data
+= td("", colspan
=2)
591 state
, builds
= b
.getState()
592 if state
!= "offline":
593 text
+= "%s<br />\n" % state
#b.getCurrentBig().text[0]
595 text
+= "OFFLINE<br />\n"
597 data
+= td(text
, align
="center", bgcolor
=color
)
599 # the next row has the column headers: time, changes, builder names
601 data
+= td("Time", align
="center")
602 data
+= td("Changes", align
="center")
605 "<a href=\"%s\">%s</a>" % (urllib
.quote(request
.childLink(name
)), name
),
609 # all further rows involve timestamps, commit events, and build events
611 data
+= td("04:00", align
="bottom")
612 data
+= td("fred", align
="center")
614 data
+= td("stuff", align
="center", bgcolor
="red")
620 def buildGrid(self
, request
, builders
):
622 # TODO: see if we can use a cached copy
625 if request
.args
.get("show_events", ["true"])[0].lower() == "true":
627 filterBranches
= [b
for b
in request
.args
.get("branch", []) if b
]
628 maxTime
= int(request
.args
.get("last_time", [util
.now()])[0])
629 if "show_time" in request
.args
:
630 minTime
= maxTime
- int(request
.args
["show_time"][0])
631 elif "first_time" in request
.args
:
632 minTime
= int(request
.args
["first_time"][0])
635 spanLength
= 10 # ten-second chunks
636 maxPageLen
= int(request
.args
.get("num_events", [200])[0])
638 # first step is to walk backwards in time, asking each column
639 # (commit, all builders) if they have any events there. Build up the
640 # array of events, and stop when we have a reasonable number.
642 commit_source
= self
.getChangemaster(request
)
644 lastEventTime
= util
.now()
645 sources
= [commit_source
] + builders
646 changeNames
= ["changes"]
647 builderNames
= map(lambda builder
: builder
.getName(), builders
)
648 sourceNames
= changeNames
+ builderNames
650 sourceGenerators
= []
652 def get_event_from(g
):
656 if not showEvents
and isinstance(e
, builder
.Event
):
659 event
= interfaces
.IStatusEvent(e
)
661 log
.msg("gen %s gave1 %s" % (g
, event
.getText()))
662 except StopIteration:
667 gen
= insertGaps(s
.eventGenerator(filterBranches
), lastEventTime
)
668 sourceGenerators
.append(gen
)
669 # get the first event
670 sourceEvents
.append(get_event_from(gen
))
675 for e
in sourceEvents
:
676 if e
and e
.getTimes()[0] > lastEventTime
:
677 lastEventTime
= e
.getTimes()[0]
678 if lastEventTime
== 0:
679 lastEventTime
= util
.now()
681 spanStart
= lastEventTime
- spanLength
685 if debugGather
: log
.msg("checking (%s,]" % spanStart
)
686 # the tableau of potential events is in sourceEvents[]. The
687 # window crawls backwards, and we examine one source at a time.
688 # If the source's top-most event is in the window, is it pushed
689 # onto the events[] array and the tableau is refilled. This
690 # continues until the tableau event is not in the window (or is
693 spanEvents
= [] # for all sources, in this span. row of eventGrid
694 firstTimestamp
= None # timestamp of first event in the span
695 lastTimestamp
= None # last pre-span event, for next span
697 for c
in range(len(sourceGenerators
)):
698 events
= [] # for this source, in this span. cell of eventGrid
699 event
= sourceEvents
[c
]
700 while event
and spanStart
< event
.getTimes()[0]:
701 # to look at windows that don't end with the present,
702 # condition the .append on event.time <= spanFinish
703 if not IBox(event
, None):
704 log
.msg("BAD EVENT", event
, event
.getText())
707 log
.msg("pushing", event
.getText(), event
)
709 starts
, finishes
= event
.getTimes()
710 firstTimestamp
= util
.earlier(firstTimestamp
, starts
)
711 event
= get_event_from(sourceGenerators
[c
])
713 log
.msg("finished span")
716 # this is the last pre-span event for this source
717 lastTimestamp
= util
.later(lastTimestamp
,
720 log
.msg(" got %s from %s" % (events
, sourceNames
[c
]))
721 sourceEvents
[c
] = event
# refill the tableau
722 spanEvents
.append(events
)
724 # only show events older than maxTime. This makes it possible to
725 # visit a page that shows what it would be like to scroll off the
726 # bottom of this one.
727 if firstTimestamp
is not None and firstTimestamp
<= maxTime
:
728 eventGrid
.append(spanEvents
)
729 timestamps
.append(firstTimestamp
)
732 spanStart
= lastTimestamp
- spanLength
736 if minTime
is not None and lastTimestamp
< minTime
:
739 if len(timestamps
) > maxPageLen
:
745 # loop is finished. now we have eventGrid[] and timestamps[]
746 if debugGather
: log
.msg("finished loop")
747 assert(len(timestamps
) == len(eventGrid
))
748 return (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
)
750 def phase0(self
, request
, sourceNames
, timestamps
, eventGrid
):
755 for r
in range(0, len(timestamps
)):
757 data
+= "[%s]<br />" % timestamps
[r
]
759 assert(len(row
) == len(sourceNames
))
760 for c
in range(0, len(row
)):
762 data
+= "<b>%s</b><br />\n" % sourceNames
[c
]
764 log
.msg("Event", r
, c
, sourceNames
[c
], e
.getText())
765 lognames
= [loog
.getName() for loog
in e
.getLogs()]
766 data
+= "%s: %s: %s %s<br />" % (e
.getText(),
771 data
+= "<b>%s</b> [none]<br />\n" % sourceNames
[c
]
774 def phase1(self
, request
, sourceNames
, timestamps
, eventGrid
,
776 # phase1 rendering: table, but boxes do not overlap
781 for r
in range(0, len(timestamps
)):
782 chunkstrip
= eventGrid
[r
]
783 # chunkstrip is a horizontal strip of event blocks. Each block
784 # is a vertical list of events, all for the same source.
785 assert(len(chunkstrip
) == len(sourceNames
))
786 maxRows
= reduce(lambda x
,y
: max(x
,y
),
787 map(lambda x
: len(x
), chunkstrip
))
788 for i
in range(maxRows
):
792 # add the date at the beginning, and each time it changes
793 today
= time
.strftime("<b>%d %b %Y</b>",
794 time
.localtime(timestamps
[r
]))
795 todayday
= time
.strftime("<b>%a</b>",
796 time
.localtime(timestamps
[r
]))
797 if today
!= lastDate
:
798 stuff
.append(todayday
)
802 time
.strftime("%H:%M:%S",
803 time
.localtime(timestamps
[r
])))
804 data
+= td(stuff
, valign
="bottom", align
="center",
805 rowspan
=maxRows
, class_
="Time")
806 for c
in range(0, len(chunkstrip
)):
807 block
= chunkstrip
[c
]
808 assert(block
!= None) # should be [] instead
810 offset
= maxRows
- len(block
)
815 box
= IBox(e
).getBox()
816 box
.parms
["show_idle"] = 1
817 data
+= box
.td(valign
="top", align
="center")
822 def phase2(self
, request
, sourceNames
, timestamps
, eventGrid
,
827 # first pass: figure out the height of the chunks, populate grid
829 for i
in range(1+len(sourceNames
)):
831 # grid is a list of columns, one for the timestamps, and one per
832 # event source. Each column is exactly the same height. Each element
833 # of the list is a single <td> box.
834 lastDate
= time
.strftime("<b>%d %b %Y</b>",
835 time
.localtime(util
.now()))
836 for r
in range(0, len(timestamps
)):
837 chunkstrip
= eventGrid
[r
]
838 # chunkstrip is a horizontal strip of event blocks. Each block
839 # is a vertical list of events, all for the same source.
840 assert(len(chunkstrip
) == len(sourceNames
))
841 maxRows
= reduce(lambda x
,y
: max(x
,y
),
842 map(lambda x
: len(x
), chunkstrip
))
843 for i
in range(maxRows
):
847 # timestamp goes at the bottom of the chunk
849 # add the date at the beginning (if it is not the same as
850 # today's date), and each time it changes
851 todayday
= time
.strftime("<b>%a</b>",
852 time
.localtime(timestamps
[r
]))
853 today
= time
.strftime("<b>%d %b %Y</b>",
854 time
.localtime(timestamps
[r
]))
855 if today
!= lastDate
:
856 stuff
.append(todayday
)
860 time
.strftime("%H:%M:%S",
861 time
.localtime(timestamps
[r
])))
862 grid
[0].append(Box(text
=stuff
, class_
="Time",
863 valign
="bottom", align
="center"))
865 # at this point the timestamp column has been populated with
866 # maxRows boxes, most None but the last one has the time string
867 for c
in range(0, len(chunkstrip
)):
868 block
= chunkstrip
[c
]
869 assert(block
!= None) # should be [] instead
870 for i
in range(maxRows
- len(block
)):
871 # fill top of chunk with blank space
872 grid
[c
+1].append(None)
873 for i
in range(len(block
)):
874 # so the events are bottom-justified
875 b
= IBox(block
[i
]).getBox()
876 b
.parms
['valign'] = "top"
877 b
.parms
['align'] = "center"
879 # now all the other columns have maxRows new boxes too
880 # populate the last row, if empty
881 gridlen
= len(grid
[0])
882 for i
in range(len(grid
)):
884 assert(len(strip
) == gridlen
)
885 if strip
[-1] == None:
886 if sourceEvents
[i
-1]:
887 filler
= IBox(sourceEvents
[i
-1]).getBox()
889 # this can happen if you delete part of the build history
890 filler
= Box(text
=["?"], align
="center")
892 strip
[-1].parms
['rowspan'] = 1
893 # second pass: bubble the events upwards to un-occupied locations
894 # Every square of the grid that has a None in it needs to have
895 # something else take its place.
896 noBubble
= request
.args
.get("nobubble",['0'])
897 noBubble
= int(noBubble
[0])
899 for col
in range(len(grid
)):
901 if col
== 1: # changes are handled differently
902 for i
in range(2, len(strip
)+1):
903 # only merge empty boxes. Don't bubble commit boxes.
904 if strip
[-i
] == None:
910 # bubble the empty box up
912 strip
[-i
].parms
['rowspan'] += 1
915 # we are above a commit box. Leave it
916 # be, and turn the current box into an
918 strip
[-i
] = Box([], rowspan
=1,
919 comment
="commit bubble")
920 strip
[-i
].spacer
= True
922 # we are above another empty box, which
923 # somehow wasn't already converted.
927 for i
in range(2, len(strip
)+1):
928 # strip[-i] will go from next-to-last back to first
929 if strip
[-i
] == None:
930 # bubble previous item up
931 assert(strip
[-i
+1] != None)
932 strip
[-i
] = strip
[-i
+1]
933 strip
[-i
].parms
['rowspan'] += 1
936 strip
[-i
].parms
['rowspan'] = 1
937 # third pass: render the HTML table
938 for i
in range(gridlen
):
947 # Nones are left empty, rowspan should make it all fit