Skip to content

Commit

Permalink
Refactoring of the extraction of build data from the info page in Hud…
Browse files Browse the repository at this point in the history
…son.

The data is stored in `Build` dicts and reused in the summary page.
Somewhat primitive still but works.
  • Loading branch information
cboos committed Jun 11, 2010
1 parent 4e9abe0 commit e9e0e46
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 86 deletions.
178 changes: 99 additions & 79 deletions HudsonTrac/HudsonTracPlugin.py
Expand Up @@ -14,7 +14,7 @@
import time
import urllib2
from xml.dom import minidom
from datetime import datetime
from datetime import datetime, timedelta

from pkg_resources import resource_filename

Expand All @@ -32,6 +32,10 @@

add_domain, _, tag_ = domain_functions('hudsontrac', 'add_domain', '_', 'tag_')

class Build(dict):
def __getattr__(self, name):
return self[name]

class HudsonTracPlugin(Component):
"""Display Hudson results in the timeline and an entry in the main
navigation bar.
Expand Down Expand Up @@ -115,74 +119,14 @@ def __init__(self):

self.env.log.debug("Build-info url: '%s'", self.info_url)

# IPermissionRequestor methods

def get_permission_actions(self):
return ['BUILD_VIEW']

# INavigationContributor methods

def get_active_navigation_item(self, req):
return 'builds'

def get_navigation_items(self, req):
if req.perm.has_permission('BUILD_VIEW'):
yield ('mainnav', 'builds',
tag.a(_("Build"),
href=self.nav_url or req.href('hudson-build'),
target=self.disp_tab and 'hudson' or None))

# ITemplateProvider methods

def get_templates_dirs(self):
return [resource_filename(__name__, 'templates')]

def get_htdocs_dirs(self):
return [('HudsonTrac', resource_filename(__name__, 'htdocs'))]

# IRequestHandler methods

def match_request(self, req):
return req.path_info.startswith('/hudson-build')

def process_request(self, req):
data = {}
return 'hudson-build.html', data, None

# ITimelineEventProvider methods

def get_timeline_filters(self, req):
if 'BUILD_VIEW' in req.perm:
yield ('build', _('Hudson Builds'))

def get_timeline_events(self, req, start, stop, filters):
if 'build' not in filters or 'BUILD_VIEW' not in req.perm:
return

# xml parsing helpers
def get_text(node):
rc = ""
for node in node.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return rc

def get_string(parent, child):
nodes = parent.getElementsByTagName(child)
return nodes and get_text(nodes[0]).strip() or ''

def get_number(parent, child):
num = get_string(parent, child)
return num and int(num) or 0

def _get_info(self, start, stop):
"""Retrieve build information from Hudson, in the given period."""
start = to_timestamp(start)
stop = to_timestamp(stop)

# get and parse the build-info
url = self.info_url % {'start': start*1000, 'stop': stop*1000}
try:
try:
info = minidom.parse(self.url_opener.open(url))
return minidom.parse(self.url_opener.open(url))
except Exception:
import sys
self.env.log.exception("Error getting build info from '%s'",
Expand All @@ -194,13 +138,32 @@ def get_number(parent, child):
finally:
self.url_opener.close()

def _extract_builds(self, info):
"""Extract build information from XML retrieved from Hudson.
Returns a list of build dicts.
"""
if info.documentElement.nodeName != 'builds':
raise IOError(
"Error getting build info from '%s': returned document has "
"unexpected node '%s'. This most likely means you configured "
"a wrong job_url" % (info_url, info.documentElement.nodeName))

add_stylesheet(req, 'HudsonTrac/hudsontrac.css')
# xml parsing helpers
def get_text(node):
rc = ""
for node in node.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return rc

def get_string(parent, child):
nodes = parent.getElementsByTagName(child)
return nodes and get_text(nodes[0]).strip() or ''

def get_number(parent, child):
num = get_string(parent, child)
return num and int(num) or 0

# extract all build entries
for entry in info.documentElement.getElementsByTagName("build"):
Expand Down Expand Up @@ -236,25 +199,82 @@ def get_number(parent, child):
if self.use_desc:
message = get_string(entry, 'description') or message

author = get_string(entry, 'fullName')
data = (get_string(entry, 'fullDisplayName'),
get_string(entry, 'url'),
result, message, started, completed)
yield kind, completed, author, data
name = get_string(entry, 'fullDisplayName')
module = name[:name.rindex('#')]
yield Build(name=name, module=module,
url=get_string(entry, 'url'),
author=get_string(entry, 'fullName'),
result=result, message=message, started=started,
completed=completed, kind=kind)

# IPermissionRequestor methods

def get_permission_actions(self):
return ['BUILD_VIEW']

# INavigationContributor methods

def get_active_navigation_item(self, req):
return 'builds'

def get_navigation_items(self, req):
if req.perm.has_permission('BUILD_VIEW'):
yield ('mainnav', 'builds',
tag.a(_("Build"),
href=self.nav_url or req.href('hudson-build'),
target=self.disp_tab and 'hudson' or None))

# ITemplateProvider methods

def get_templates_dirs(self):
return [resource_filename(__name__, 'templates')]

def get_htdocs_dirs(self):
return [('HudsonTrac', resource_filename(__name__, 'htdocs'))]

# IRequestHandler methods

def match_request(self, req):
return req.path_info.startswith('/hudson-build')

def process_request(self, req):
stop = datetime.now(req.tz)
start = stop - timedelta(days=30)
builds = self._extract_builds(self._get_info(start, stop))
data = {'builds': builds}
add_stylesheet(req, 'HudsonTrac/hudsontrac.css')
return 'hudson-build.html', data, None

# ITimelineEventProvider methods

def get_timeline_filters(self, req):
if 'BUILD_VIEW' in req.perm:
yield ('build', _('Hudson Builds'))

def get_timeline_events(self, req, start, stop, filters):
if 'build' not in filters or 'BUILD_VIEW' not in req.perm:
return

add_stylesheet(req, 'HudsonTrac/hudsontrac.css')

# get and parse the build-info
info = self._get_info(start, stop)
for b in self._extract_builds(info):
self.log.debug("Build: %r", b)
yield b.kind, b.completed, b.author, b

def render_timeline_event(self, context, field, event):
kind, completed, author, data = event
name, url, result, message, started, completed = data
b = event[3]
if field == 'title':
return tag_('Build %(name)s (%(result)s)', name=tag.em(name),
result=result.lower())
return tag_('Build %(name)s (%(result)s)', name=tag.em(b.name),
result=b.result.lower())
elif field == 'description':
elapsed = pretty_timedelta(started, completed)
if kind == 'build-inprogress':
return _("%(message)s since %(elapsed)s", message=message,
elapsed = pretty_timedelta(b.started, b.completed)
if b.kind == 'build-inprogress':
return _("%(message)s since %(elapsed)s", message=b.message,
elapsed=elapsed)
else:
return _("%(message)s after %(elapsed)s", message=message,
return _("%(message)s after %(elapsed)s", message=b.message,
elapsed=elapsed)
elif field == 'url':
return url
return b.url
29 changes: 23 additions & 6 deletions HudsonTrac/htdocs/hudsontrac.css
@@ -1,6 +1,23 @@
dt.build-successful a { background-image: url(success.gif) !important }
dt.build-successful-alt a { background-image: url(success-alt.gif) !important }
dt.build-failed a { background-image: url(failed.gif) !important }
dt.build-aborted a { background-image: url(aborted.gif) !important }
dt.build-unstable a { background-image: url(unstable.gif) !important }
dt.build-inprogress a { background-image: url(inprogress.gif) !important }
.build-successful a { background-image: url(success.gif) !important }
.build-successful-alt a { background-image: url(success-alt.gif) !important }
.build-failed a { background-image: url(failed.gif) !important }
.build-aborted a { background-image: url(aborted.gif) !important }
.build-unstable a { background-image: url(unstable.gif) !important }
.build-inprogress a { background-image: url(inprogress.gif) !important }

ul.builds {
list-style: none;
}
ul.builds li {
display: inline;
}
ul.builds li {
line-height: 20px;
margin-right: 2px;
/* border: 1px solid blue; */
}
ul.builds li a {
display: inline;
background-repeat: no-repeat;
padding: 0 14px 0 0;
}
31 changes: 30 additions & 1 deletion HudsonTrac/templates/hudson-build.html
@@ -1 +1,30 @@
<html><body>Nothing to see yet ;-)</body></html>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:i18n="http://genshi.edgewall.org/i18n">
<xi:include href="layout.html" />
<head>
<title>Hudson Build Summary</title>
</head>
<body>
<div id="content" class="timeline">
<h1>Hudson Build Summary</h1>
<py:for each="project, builds in groupby(builds, key=lambda b: b.module)">
<h2>Recent builds for: ${project}</h2>
<ul class="builds">
<py:for each="b in builds">
<li class="${b.kind}">
<a href="${b.url}" title="${format_datetime(b.started)}"></a>
</li>
</py:for>
</ul>
</py:for>
</div>
<div id="help" i18n:msg="">
<strong>Note:</strong> Most recent builds are on the left side.
</div>
</body>
</html>

0 comments on commit e9e0e46

Please sign in to comment.