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
307 <h2>Reload Waterfall Page</h2>
309 <input type="submit" value="View Waterfall" />
313 class WaterfallHelp(HtmlResource
):
314 title
= "Waterfall Help"
316 def __init__(self
, categories
=None):
317 HtmlResource
.__init
__(self
)
318 self
.categories
= categories
320 def body(self
, request
):
322 status
= self
.getStatus(request
)
324 showEvents_checked
= 'checked="checked"'
325 if request
.args
.get("show_events", ["true"])[0].lower() == "true":
326 showEvents_checked
= ''
327 show_events_input
= ('<p>'
328 '<input type="checkbox" name="show_events" '
330 'Hide non-Build events'
332 ) % showEvents_checked
335 for b
in request
.args
.get("branch", [])
338 show_branches_input
= '<table>\n'
340 show_branches_input
+= ('<tr>'
342 '<input type="text" name="branch" '
346 show_branches_input
+= '</table>\n'
348 # this has a set of toggle-buttons to let the user choose the
350 showBuilders
= request
.args
.get("show", [])
351 showBuilders
.extend(request
.args
.get("builder", []))
352 allBuilders
= status
.getBuilderNames(categories
=self
.categories
)
354 show_builders_input
= '<table>\n'
355 for bn
in allBuilders
:
357 if bn
in showBuilders
:
358 checked
= 'checked="checked"'
359 show_builders_input
+= ('<tr>'
360 '<td><input type="checkbox"'
362 'value="%s" %s></td> '
364 ) % (bn
, checked
, bn
)
365 show_builders_input
+= '</table>\n'
367 # a couple of radio-button selectors for refresh time will appear
368 # just after that text
369 show_reload_input
= '<table>\n'
370 times
= [("none", "None"),
371 ("60", "60 seconds"),
372 ("300", "5 minutes"),
373 ("600", "10 minutes"),
375 current_reload_time
= request
.args
.get("reload", ["none"])
376 if current_reload_time
:
377 current_reload_time
= current_reload_time
[0]
378 if current_reload_time
not in [t
[0] for t
in times
]:
379 times
.insert(0, (current_reload_time
, current_reload_time
) )
380 for value
, name
in times
:
382 if value
== current_reload_time
:
383 checked
= 'checked="checked"'
384 show_reload_input
+= ('<tr>'
385 '<td><input type="radio" name="reload" '
386 'value="%s" %s></td> '
388 ) % (value
, checked
, name
)
389 show_reload_input
+= '</table>\n'
391 fields
= {"show_events_input": show_events_input
,
392 "show_branches_input": show_branches_input
,
393 "show_builders_input": show_builders_input
,
394 "show_reload_input": show_reload_input
,
396 data
+= HELP
% fields
399 class WaterfallStatusResource(HtmlResource
):
400 """This builds the main status page, with the waterfall display, and
403 def __init__(self
, categories
=None):
404 HtmlResource
.__init
__(self
)
405 self
.categories
= categories
406 self
.putChild("help", WaterfallHelp(categories
))
408 def getTitle(self
, request
):
409 status
= self
.getStatus(request
)
410 p
= status
.getProjectName()
412 return "BuildBot: %s" % p
416 def getChangemaster(self
, request
):
417 # TODO: this wants to go away, access it through IStatus
418 return request
.site
.buildbot_service
.parent
.change_svc
420 def get_reload_time(self
, request
):
421 if "reload" in request
.args
:
423 reload_time
= int(request
.args
["reload"][0])
430 def head(self
, request
):
432 reload_time
= self
.get_reload_time(request
)
433 if reload_time
is not None:
434 head
+= '<meta http-equiv="refresh" content="%d">\n' % reload_time
437 def body(self
, request
):
438 "This method builds the main waterfall display."
440 status
= self
.getStatus(request
)
443 projectName
= status
.getProjectName()
444 projectURL
= status
.getProjectURL()
446 phase
= request
.args
.get("phase",["2"])
447 phase
= int(phase
[0])
449 showBuilders
= request
.args
.get("show", [])
450 showBuilders
.extend(request
.args
.get("builder", []))
451 allBuilders
= status
.getBuilderNames(categories
=self
.categories
)
454 for b
in showBuilders
:
455 if b
not in allBuilders
:
457 if b
in builderNames
:
459 builderNames
.append(b
)
461 builderNames
= allBuilders
462 builders
= map(lambda name
: status
.getBuilder(name
),
466 return self
.body0(request
, builders
)
467 (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
) = \
468 self
.buildGrid(request
, builders
)
470 return self
.phase0(request
, (changeNames
+ builderNames
),
471 timestamps
, eventGrid
)
472 # start the table: top-header material
473 data
+= '<table border="0" cellspacing="0">\n'
475 if projectName
and projectURL
:
476 # TODO: this is going to look really ugly
477 topleft
= "<a href=\"%s\">%s</a><br />last build" % \
478 (projectURL
, projectName
)
480 topleft
= "last build"
481 data
+= ' <tr class="LastBuild">\n'
482 data
+= td(topleft
, align
="right", colspan
=2, class_
="Project")
484 box
= ITopBox(b
).getBox()
485 data
+= box
.td(align
="center")
488 data
+= ' <tr class="Activity">\n'
489 data
+= td('current activity', align
='right', colspan
=2)
491 box
= ICurrentBox(b
).getBox(status
)
492 data
+= box
.td(align
="center")
496 TZ
= time
.tzname
[time
.daylight
]
497 data
+= td("time (%s)" % TZ
, align
="center", class_
="Time")
498 name
= changeNames
[0]
500 "<a href=\"%s\">%s</a>" % (urllib
.quote(name
, safe
=''), name
),
501 align
="center", class_
="Change")
502 for name
in builderNames
:
503 safename
= urllib
.quote(name
, safe
='')
504 data
+= td( "<a href=\"builders/%s\">%s</a>" % (safename
, name
),
505 align
="center", class_
="Builder")
512 data
+= f(request
, changeNames
+ builderNames
, timestamps
, eventGrid
,
519 def with_args(req
, remove_args
=[], new_args
=[], new_path
=None):
520 # sigh, nevow makes this sort of manipulation easier
521 newargs
= req
.args
.copy()
522 for argname
in remove_args
:
523 newargs
[argname
] = []
524 if "branch" in newargs
:
525 newargs
["branch"] = [b
for b
in newargs
["branch"] if b
]
531 newquery
= "&".join(["%s=%s" % (k
, v
)
538 new_url
= req
.prepath
[-1]
542 new_url
+= "?" + newquery
546 bottom
= timestamps
[-1]
547 nextpage
= with_args(request
, ["last_time"],
548 [("last_time", str(int(bottom
)))])
549 data
+= '[<a href="%s">next page</a>]\n' % nextpage
551 helpurl
= self
.path_to_root(request
) + "waterfall/help"
552 helppage
= with_args(request
, new_path
=helpurl
)
553 data
+= '[<a href="%s">help</a>]\n' % helppage
555 welcomeurl
= self
.path_to_root(request
) + "."
556 data
+= '[<a href="%s">welcome</a>]\n' % welcomeurl
558 if self
.get_reload_time(request
) is not None:
559 no_reload_page
= with_args(request
, remove_args
=["reload"])
560 data
+= '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page
565 bburl
= "http://buildbot.net/?bb-ver=%s" % urllib
.quote(version
)
566 data
+= "<a href=\"%s\">Buildbot-%s</a> " % (bburl
, version
)
568 data
+= "working for the "
570 data
+= "<a href=\"%s\">%s</a> project." % (projectURL
,
573 data
+= "%s project." % projectName
575 # TODO: push this to the right edge, if possible
576 data
+= ("Page built: " +
577 time
.strftime("%a %d %b %Y %H:%M:%S",
578 time
.localtime(util
.now()))
582 def body0(self
, request
, builders
):
583 # build the waterfall display
585 data
+= "<h2>Basic display</h2>\n"
586 data
+= "<p>See <a href=\"%s\">here</a>" % \
587 urllib
.quote(request
.childLink("waterfall"))
588 data
+= " for the waterfall display</p>\n"
590 data
+= '<table border="0" cellspacing="0">\n'
591 names
= map(lambda builder
: builder
.name
, builders
)
593 # the top row is two blank spaces, then the top-level status boxes
595 data
+= td("", colspan
=2)
599 state
, builds
= b
.getState()
600 if state
!= "offline":
601 text
+= "%s<br />\n" % state
#b.getCurrentBig().text[0]
603 text
+= "OFFLINE<br />\n"
605 data
+= td(text
, align
="center", bgcolor
=color
)
607 # the next row has the column headers: time, changes, builder names
609 data
+= td("Time", align
="center")
610 data
+= td("Changes", align
="center")
613 "<a href=\"%s\">%s</a>" % (urllib
.quote(request
.childLink(name
)), name
),
617 # all further rows involve timestamps, commit events, and build events
619 data
+= td("04:00", align
="bottom")
620 data
+= td("fred", align
="center")
622 data
+= td("stuff", align
="center", bgcolor
="red")
628 def buildGrid(self
, request
, builders
):
630 # TODO: see if we can use a cached copy
633 if request
.args
.get("show_events", ["true"])[0].lower() == "true":
635 filterBranches
= [b
for b
in request
.args
.get("branch", []) if b
]
636 maxTime
= int(request
.args
.get("last_time", [util
.now()])[0])
637 if "show_time" in request
.args
:
638 minTime
= maxTime
- int(request
.args
["show_time"][0])
639 elif "first_time" in request
.args
:
640 minTime
= int(request
.args
["first_time"][0])
643 spanLength
= 10 # ten-second chunks
644 maxPageLen
= int(request
.args
.get("num_events", [200])[0])
646 # first step is to walk backwards in time, asking each column
647 # (commit, all builders) if they have any events there. Build up the
648 # array of events, and stop when we have a reasonable number.
650 commit_source
= self
.getChangemaster(request
)
652 lastEventTime
= util
.now()
653 sources
= [commit_source
] + builders
654 changeNames
= ["changes"]
655 builderNames
= map(lambda builder
: builder
.getName(), builders
)
656 sourceNames
= changeNames
+ builderNames
658 sourceGenerators
= []
660 def get_event_from(g
):
664 if not showEvents
and isinstance(e
, builder
.Event
):
667 event
= interfaces
.IStatusEvent(e
)
669 log
.msg("gen %s gave1 %s" % (g
, event
.getText()))
670 except StopIteration:
675 gen
= insertGaps(s
.eventGenerator(filterBranches
), lastEventTime
)
676 sourceGenerators
.append(gen
)
677 # get the first event
678 sourceEvents
.append(get_event_from(gen
))
683 for e
in sourceEvents
:
684 if e
and e
.getTimes()[0] > lastEventTime
:
685 lastEventTime
= e
.getTimes()[0]
686 if lastEventTime
== 0:
687 lastEventTime
= util
.now()
689 spanStart
= lastEventTime
- spanLength
693 if debugGather
: log
.msg("checking (%s,]" % spanStart
)
694 # the tableau of potential events is in sourceEvents[]. The
695 # window crawls backwards, and we examine one source at a time.
696 # If the source's top-most event is in the window, is it pushed
697 # onto the events[] array and the tableau is refilled. This
698 # continues until the tableau event is not in the window (or is
701 spanEvents
= [] # for all sources, in this span. row of eventGrid
702 firstTimestamp
= None # timestamp of first event in the span
703 lastTimestamp
= None # last pre-span event, for next span
705 for c
in range(len(sourceGenerators
)):
706 events
= [] # for this source, in this span. cell of eventGrid
707 event
= sourceEvents
[c
]
708 while event
and spanStart
< event
.getTimes()[0]:
709 # to look at windows that don't end with the present,
710 # condition the .append on event.time <= spanFinish
711 if not IBox(event
, None):
712 log
.msg("BAD EVENT", event
, event
.getText())
715 log
.msg("pushing", event
.getText(), event
)
717 starts
, finishes
= event
.getTimes()
718 firstTimestamp
= util
.earlier(firstTimestamp
, starts
)
719 event
= get_event_from(sourceGenerators
[c
])
721 log
.msg("finished span")
724 # this is the last pre-span event for this source
725 lastTimestamp
= util
.later(lastTimestamp
,
728 log
.msg(" got %s from %s" % (events
, sourceNames
[c
]))
729 sourceEvents
[c
] = event
# refill the tableau
730 spanEvents
.append(events
)
732 # only show events older than maxTime. This makes it possible to
733 # visit a page that shows what it would be like to scroll off the
734 # bottom of this one.
735 if firstTimestamp
is not None and firstTimestamp
<= maxTime
:
736 eventGrid
.append(spanEvents
)
737 timestamps
.append(firstTimestamp
)
740 spanStart
= lastTimestamp
- spanLength
744 if minTime
is not None and lastTimestamp
< minTime
:
747 if len(timestamps
) > maxPageLen
:
753 # loop is finished. now we have eventGrid[] and timestamps[]
754 if debugGather
: log
.msg("finished loop")
755 assert(len(timestamps
) == len(eventGrid
))
756 return (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
)
758 def phase0(self
, request
, sourceNames
, timestamps
, eventGrid
):
763 for r
in range(0, len(timestamps
)):
765 data
+= "[%s]<br />" % timestamps
[r
]
767 assert(len(row
) == len(sourceNames
))
768 for c
in range(0, len(row
)):
770 data
+= "<b>%s</b><br />\n" % sourceNames
[c
]
772 log
.msg("Event", r
, c
, sourceNames
[c
], e
.getText())
773 lognames
= [loog
.getName() for loog
in e
.getLogs()]
774 data
+= "%s: %s: %s %s<br />" % (e
.getText(),
779 data
+= "<b>%s</b> [none]<br />\n" % sourceNames
[c
]
782 def phase1(self
, request
, sourceNames
, timestamps
, eventGrid
,
784 # phase1 rendering: table, but boxes do not overlap
789 for r
in range(0, len(timestamps
)):
790 chunkstrip
= eventGrid
[r
]
791 # chunkstrip is a horizontal strip of event blocks. Each block
792 # is a vertical list of events, all for the same source.
793 assert(len(chunkstrip
) == len(sourceNames
))
794 maxRows
= reduce(lambda x
,y
: max(x
,y
),
795 map(lambda x
: len(x
), chunkstrip
))
796 for i
in range(maxRows
):
800 # add the date at the beginning, and each time it changes
801 today
= time
.strftime("<b>%d %b %Y</b>",
802 time
.localtime(timestamps
[r
]))
803 todayday
= time
.strftime("<b>%a</b>",
804 time
.localtime(timestamps
[r
]))
805 if today
!= lastDate
:
806 stuff
.append(todayday
)
810 time
.strftime("%H:%M:%S",
811 time
.localtime(timestamps
[r
])))
812 data
+= td(stuff
, valign
="bottom", align
="center",
813 rowspan
=maxRows
, class_
="Time")
814 for c
in range(0, len(chunkstrip
)):
815 block
= chunkstrip
[c
]
816 assert(block
!= None) # should be [] instead
818 offset
= maxRows
- len(block
)
823 box
= IBox(e
).getBox()
824 box
.parms
["show_idle"] = 1
825 data
+= box
.td(valign
="top", align
="center")
830 def phase2(self
, request
, sourceNames
, timestamps
, eventGrid
,
835 # first pass: figure out the height of the chunks, populate grid
837 for i
in range(1+len(sourceNames
)):
839 # grid is a list of columns, one for the timestamps, and one per
840 # event source. Each column is exactly the same height. Each element
841 # of the list is a single <td> box.
842 lastDate
= time
.strftime("<b>%d %b %Y</b>",
843 time
.localtime(util
.now()))
844 for r
in range(0, len(timestamps
)):
845 chunkstrip
= eventGrid
[r
]
846 # chunkstrip is a horizontal strip of event blocks. Each block
847 # is a vertical list of events, all for the same source.
848 assert(len(chunkstrip
) == len(sourceNames
))
849 maxRows
= reduce(lambda x
,y
: max(x
,y
),
850 map(lambda x
: len(x
), chunkstrip
))
851 for i
in range(maxRows
):
855 # timestamp goes at the bottom of the chunk
857 # add the date at the beginning (if it is not the same as
858 # today's date), and each time it changes
859 todayday
= time
.strftime("<b>%a</b>",
860 time
.localtime(timestamps
[r
]))
861 today
= time
.strftime("<b>%d %b %Y</b>",
862 time
.localtime(timestamps
[r
]))
863 if today
!= lastDate
:
864 stuff
.append(todayday
)
868 time
.strftime("%H:%M:%S",
869 time
.localtime(timestamps
[r
])))
870 grid
[0].append(Box(text
=stuff
, class_
="Time",
871 valign
="bottom", align
="center"))
873 # at this point the timestamp column has been populated with
874 # maxRows boxes, most None but the last one has the time string
875 for c
in range(0, len(chunkstrip
)):
876 block
= chunkstrip
[c
]
877 assert(block
!= None) # should be [] instead
878 for i
in range(maxRows
- len(block
)):
879 # fill top of chunk with blank space
880 grid
[c
+1].append(None)
881 for i
in range(len(block
)):
882 # so the events are bottom-justified
883 b
= IBox(block
[i
]).getBox()
884 b
.parms
['valign'] = "top"
885 b
.parms
['align'] = "center"
887 # now all the other columns have maxRows new boxes too
888 # populate the last row, if empty
889 gridlen
= len(grid
[0])
890 for i
in range(len(grid
)):
892 assert(len(strip
) == gridlen
)
893 if strip
[-1] == None:
894 if sourceEvents
[i
-1]:
895 filler
= IBox(sourceEvents
[i
-1]).getBox()
897 # this can happen if you delete part of the build history
898 filler
= Box(text
=["?"], align
="center")
900 strip
[-1].parms
['rowspan'] = 1
901 # second pass: bubble the events upwards to un-occupied locations
902 # Every square of the grid that has a None in it needs to have
903 # something else take its place.
904 noBubble
= request
.args
.get("nobubble",['0'])
905 noBubble
= int(noBubble
[0])
907 for col
in range(len(grid
)):
909 if col
== 1: # changes are handled differently
910 for i
in range(2, len(strip
)+1):
911 # only merge empty boxes. Don't bubble commit boxes.
912 if strip
[-i
] == None:
918 # bubble the empty box up
920 strip
[-i
].parms
['rowspan'] += 1
923 # we are above a commit box. Leave it
924 # be, and turn the current box into an
926 strip
[-i
] = Box([], rowspan
=1,
927 comment
="commit bubble")
928 strip
[-i
].spacer
= True
930 # we are above another empty box, which
931 # somehow wasn't already converted.
935 for i
in range(2, len(strip
)+1):
936 # strip[-i] will go from next-to-last back to first
937 if strip
[-i
] == None:
938 # bubble previous item up
939 assert(strip
[-i
+1] != None)
940 strip
[-i
] = strip
[-i
+1]
941 strip
[-i
].parms
['rowspan'] += 1
944 strip
[-i
].parms
['rowspan'] = 1
945 # third pass: render the HTML table
946 for i
in range(gridlen
):
955 # Nones are left empty, rowspan should make it all fit