diff options
Diffstat (limited to 'buildbot/buildbot/status/web/base.py')
-rw-r--r-- | buildbot/buildbot/status/web/base.py | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/buildbot/buildbot/status/web/base.py b/buildbot/buildbot/status/web/base.py new file mode 100644 index 0000000..e515a25 --- /dev/null +++ b/buildbot/buildbot/status/web/base.py @@ -0,0 +1,421 @@ + +import urlparse, urllib, time +from zope.interface import Interface +from twisted.web import html, resource +from buildbot.status import builder +from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION +from buildbot import version, util + +class ITopBox(Interface): + """I represent a box in the top row of the waterfall display: the one + which shows the status of the last build for each builder.""" + def getBox(self, request): + """Return a Box instance, which can produce a <td> cell. + """ + +class ICurrentBox(Interface): + """I represent the 'current activity' box, just above the builder name.""" + def getBox(self, status): + """Return a Box instance, which can produce a <td> cell. + """ + +class IBox(Interface): + """I represent a box in the waterfall display.""" + def getBox(self, request): + """Return a Box instance, which wraps an Event and can produce a <td> + cell. + """ + +class IHTMLLog(Interface): + pass + +css_classes = {SUCCESS: "success", + WARNINGS: "warnings", + FAILURE: "failure", + SKIPPED: "skipped", + EXCEPTION: "exception", + } + +ROW_TEMPLATE = ''' +<div class="row"> + <span class="label">%(label)s</span> + <span class="field">%(field)s</span> +</div> +''' + +def make_row(label, field): + """Create a name/value row for the HTML. + + `label` is plain text; it will be HTML-encoded. + + `field` is a bit of HTML structure; it will not be encoded in + any way. + """ + label = html.escape(label) + return ROW_TEMPLATE % {"label": label, "field": field} + +def make_stop_form(stopURL, on_all=False, label="Build"): + if on_all: + data = """<form action="%s" class='command stopbuild'> + <p>To stop all builds, fill out the following fields and + push the 'Stop' button</p>\n""" % stopURL + else: + data = """<form action="%s" class='command stopbuild'> + <p>To stop this build, fill out the following fields and + push the 'Stop' button</p>\n""" % stopURL + data += make_row("Your name:", + "<input type='text' name='username' />") + data += make_row("Reason for stopping build:", + "<input type='text' name='comments' />") + data += '<input type="submit" value="Stop %s" /></form>\n' % label + return data + +def make_force_build_form(forceURL, on_all=False): + if on_all: + data = """<form action="%s" class="command forcebuild"> + <p>To force a build on all Builders, fill out the following fields + and push the 'Force Build' button</p>""" % forceURL + else: + data = """<form action="%s" class="command forcebuild"> + <p>To force a build, fill out the following fields and + push the 'Force Build' button</p>""" % forceURL + return (data + + make_row("Your name:", + "<input type='text' name='username' />") + + make_row("Reason for build:", + "<input type='text' name='comments' />") + + make_row("Branch to build:", + "<input type='text' name='branch' />") + + make_row("Revision to build:", + "<input type='text' name='revision' />") + + '<input type="submit" value="Force Build" /></form>\n') + +def td(text="", parms={}, **props): + data = "" + data += " " + #if not props.has_key("border"): + # props["border"] = 1 + props.update(parms) + comment = props.get("comment", None) + if comment: + data += "<!-- %s -->" % comment + data += "<td" + class_ = props.get('class_', None) + if class_: + props["class"] = class_ + for prop in ("align", "colspan", "rowspan", "border", + "valign", "halign", "class"): + p = props.get(prop, None) + if p != None: + data += " %s=\"%s\"" % (prop, p) + data += ">" + if not text: + text = " " + if isinstance(text, list): + data += "<br />".join(text) + else: + data += text + data += "</td>\n" + return data + +def build_get_class(b): + """ + Return the class to use for a finished build or buildstep, + based on the result. + """ + # FIXME: this getResults duplicity might need to be fixed + result = b.getResults() + #print "THOMAS: result for b %r: %r" % (b, result) + if isinstance(b, builder.BuildStatus): + result = b.getResults() + elif isinstance(b, builder.BuildStepStatus): + result = b.getResults()[0] + # after forcing a build, b.getResults() returns ((None, []), []), ugh + if isinstance(result, tuple): + result = result[0] + else: + raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b + + if result == None: + # FIXME: this happens when a buildstep is running ? + return "running" + return builder.Results[result] + +def path_to_root(request): + # /waterfall : ['waterfall'] -> '' + # /somewhere/lower : ['somewhere', 'lower'] -> '../' + # /somewhere/indexy/ : ['somewhere', 'indexy', ''] -> '../../' + # / : [] -> '' + if request.prepath: + segs = len(request.prepath) - 1 + else: + segs = 0 + root = "../" * segs + return root + +def path_to_builder(request, builderstatus): + return (path_to_root(request) + + "builders/" + + urllib.quote(builderstatus.getName(), safe='')) + +def path_to_build(request, buildstatus): + return (path_to_builder(request, buildstatus.getBuilder()) + + "/builds/%d" % buildstatus.getNumber()) + +def path_to_step(request, stepstatus): + return (path_to_build(request, stepstatus.getBuild()) + + "/steps/%s" % urllib.quote(stepstatus.getName(), safe='')) + +def path_to_slave(request, slave): + return (path_to_root(request) + + "buildslaves/" + + urllib.quote(slave.getName(), safe='')) + +class Box: + # a Box wraps an Event. The Box has HTML <td> parameters that Events + # lack, and it has a base URL to which each File's name is relative. + # Events don't know about HTML. + spacer = False + def __init__(self, text=[], class_=None, urlbase=None, + **parms): + self.text = text + self.class_ = class_ + self.urlbase = urlbase + self.show_idle = 0 + if parms.has_key('show_idle'): + del parms['show_idle'] + self.show_idle = 1 + + self.parms = parms + # parms is a dict of HTML parameters for the <td> element that will + # represent this Event in the waterfall display. + + def td(self, **props): + props.update(self.parms) + text = self.text + if not text and self.show_idle: + text = ["[idle]"] + return td(text, props, class_=self.class_) + + +class HtmlResource(resource.Resource): + # this is a cheap sort of template thingy + contentType = "text/html; charset=UTF-8" + title = "Buildbot" + addSlash = False # adapted from Nevow + + def getChild(self, path, request): + if self.addSlash and path == "" and len(request.postpath) == 0: + return self + return resource.Resource.getChild(self, path, request) + + def render(self, request): + # tell the WebStatus about the HTTPChannel that got opened, so they + # can close it if we get reconfigured and the WebStatus goes away. + # They keep a weakref to this, since chances are good that it will be + # closed by the browser or by us before we get reconfigured. See + # ticket #102 for details. + if hasattr(request, "channel"): + # web.distrib.Request has no .channel + request.site.buildbot_service.registerChannel(request.channel) + + # Our pages no longer require that their URL end in a slash. Instead, + # they all use request.childLink() or some equivalent which takes the + # last path component into account. This clause is left here for + # historical and educational purposes. + if False and self.addSlash and request.prepath[-1] != '': + # this is intended to behave like request.URLPath().child('') + # but we need a relative URL, since we might be living behind a + # reverse proxy + # + # note that the Location: header (as used in redirects) are + # required to have absolute URIs, and my attempt to handle + # reverse-proxies gracefully violates rfc2616. This frequently + # works, but single-component paths sometimes break. The best + # strategy is to avoid these redirects whenever possible by using + # HREFs with trailing slashes, and only use the redirects for + # manually entered URLs. + url = request.prePathURL() + scheme, netloc, path, query, fragment = urlparse.urlsplit(url) + new_url = request.prepath[-1] + "/" + if query: + new_url += "?" + query + request.redirect(new_url) + return '' + + data = self.content(request) + if isinstance(data, unicode): + data = data.encode("utf-8") + request.setHeader("content-type", self.contentType) + if request.method == "HEAD": + request.setHeader("content-length", len(data)) + return '' + return data + + def getStatus(self, request): + return request.site.buildbot_service.getStatus() + def getControl(self, request): + return request.site.buildbot_service.getControl() + + def getChangemaster(self, request): + return request.site.buildbot_service.getChangeSvc() + + def path_to_root(self, request): + return path_to_root(request) + + def footer(self, s, req): + # TODO: this stuff should be generated by a template of some sort + projectURL = s.getProjectURL() + projectName = s.getProjectName() + data = '<hr /><div class="footer">\n' + + welcomeurl = self.path_to_root(req) + "index.html" + data += '[<a href="%s">welcome</a>]\n' % welcomeurl + data += "<br />\n" + + data += '<a href="http://buildbot.sourceforge.net/">Buildbot</a>' + data += "-%s " % 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" + data += ("Page built: " + + time.strftime("%a %d %b %Y %H:%M:%S", + time.localtime(util.now())) + + "\n") + data += '</div>\n' + + return data + + def getTitle(self, request): + return self.title + + def fillTemplate(self, template, request): + s = request.site.buildbot_service + values = s.template_values.copy() + values['root'] = self.path_to_root(request) + # e.g. to reference the top-level 'buildbot.css' page, use + # "%(root)sbuildbot.css" + values['title'] = self.getTitle(request) + return template % values + + def content(self, request): + s = request.site.buildbot_service + data = "" + data += self.fillTemplate(s.header, request) + data += "<head>\n" + for he in s.head_elements: + data += " " + self.fillTemplate(he, request) + "\n" + data += self.head(request) + data += "</head>\n\n" + + data += '<body %s>\n' % " ".join(['%s="%s"' % (k,v) + for (k,v) in s.body_attrs.items()]) + data += self.body(request) + data += "</body>\n" + data += self.fillTemplate(s.footer, request) + return data + + def head(self, request): + return "" + + def body(self, request): + return "Dummy\n" + +class StaticHTML(HtmlResource): + def __init__(self, body, title): + HtmlResource.__init__(self) + self.bodyHTML = body + self.title = title + def body(self, request): + return self.bodyHTML + +MINUTE = 60 +HOUR = 60*MINUTE +DAY = 24*HOUR +WEEK = 7*DAY +MONTH = 30*DAY + +def plural(word, words, num): + if int(num) == 1: + return "%d %s" % (num, word) + else: + return "%d %s" % (num, words) + +def abbreviate_age(age): + if age <= 90: + return "%s ago" % plural("second", "seconds", age) + if age < 90*MINUTE: + return "about %s ago" % plural("minute", "minutes", age / MINUTE) + if age < DAY: + return "about %s ago" % plural("hour", "hours", age / HOUR) + if age < 2*WEEK: + return "about %s ago" % plural("day", "days", age / DAY) + if age < 2*MONTH: + return "about %s ago" % plural("week", "weeks", age / WEEK) + return "a long time ago" + + +class OneLineMixin: + LINE_TIME_FORMAT = "%b %d %H:%M" + + def get_line_values(self, req, build): + ''' + Collect the data needed for each line display + ''' + builder_name = build.getBuilder().getName() + results = build.getResults() + text = build.getText() + try: + rev = build.getProperty("got_revision") + if rev is None: + rev = "??" + except KeyError: + rev = "??" + rev = str(rev) + if len(rev) > 40: + rev = "version is too-long" + root = self.path_to_root(req) + css_class = css_classes.get(results, "") + values = {'class': css_class, + 'builder_name': builder_name, + 'buildnum': build.getNumber(), + 'results': css_class, + 'text': " ".join(build.getText()), + 'buildurl': path_to_build(req, build), + 'builderurl': path_to_builder(req, build.getBuilder()), + 'rev': rev, + 'time': time.strftime(self.LINE_TIME_FORMAT, + time.localtime(build.getTimes()[0])), + } + return values + + def make_line(self, req, build, include_builder=True): + ''' + Format and render a single line into HTML + ''' + values = self.get_line_values(req, build) + fmt_pieces = ['<font size="-1">(%(time)s)</font>', + 'rev=[%(rev)s]', + '<span class="%(class)s">%(results)s</span>', + ] + if include_builder: + fmt_pieces.append('<a href="%(builderurl)s">%(builder_name)s</a>') + fmt_pieces.append('<a href="%(buildurl)s">#%(buildnum)d</a>:') + fmt_pieces.append('%(text)s') + data = " ".join(fmt_pieces) % values + return data + +def map_branches(branches): + # when the query args say "trunk", present that to things like + # IBuilderStatus.generateFinishedBuilds as None, since that's the + # convention in use. But also include 'trunk', because some VC systems + # refer to it that way. In the long run we should clean this up better, + # maybe with Branch objects or something. + if "trunk" in branches: + return branches + [None] + return branches |