comparison bitten/trac_ext/web_ui.py @ 250:0271a2b1fc23

Improvements to the web interface: * The build configuration page will now display the result of the individual steps for each build. * Different icon for failed builds in the timeline. Closes #26. * The timeline events will now show which steps failed for failed builds. In the RSS feed, it'll also include the actual error messages. * The build view now displays more information about the build slave, and in a more structured way.
author cmlenz
date Tue, 04 Oct 2005 20:44:56 +0000
parents b28285d3ceec
children 42f555e1d648
comparison
equal deleted inserted replaced
249:dcba83c01266 250:0271a2b1fc23
5 # 5 #
6 # This software is licensed as described in the file COPYING, which 6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms 7 # you should have received as part of this distribution. The terms
8 # are also available at http://bitten.cmlenz.net/wiki/License. 8 # are also available at http://bitten.cmlenz.net/wiki/License.
9 9
10 from datetime import datetime, timedelta
10 import re 11 import re
11 from time import localtime, strftime 12 try:
13 set
14 except NameError:
15 from sets import Set as set
16 from StringIO import StringIO
12 17
13 import pkg_resources 18 import pkg_resources
14 from trac.core import * 19 from trac.core import *
15 from trac.Timeline import ITimelineEventProvider 20 from trac.Timeline import ITimelineEventProvider
16 from trac.util import escape, pretty_timedelta 21 from trac.util import escape, pretty_timedelta, format_date, format_datetime
17 from trac.web import IRequestHandler 22 from trac.web import IRequestHandler
18 from trac.web.chrome import INavigationContributor, ITemplateProvider, \ 23 from trac.web.chrome import INavigationContributor, ITemplateProvider, \
19 add_link, add_stylesheet 24 add_link, add_stylesheet
20 from trac.wiki import wiki_to_html 25 from trac.wiki import wiki_to_html
21 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \ 26 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \
34 'status': _status_label[build.status], 39 'status': _status_label[build.status],
35 'cls': _status_label[build.status].replace(' ', '-'), 40 'cls': _status_label[build.status].replace(' ', '-'),
36 'href': env.href.build(build.config, build.id), 41 'href': env.href.build(build.config, build.id),
37 'chgset_href': env.href.changeset(build.rev)} 42 'chgset_href': env.href.changeset(build.rev)}
38 if build.started: 43 if build.started:
39 hdf['started'] = strftime('%x %X', localtime(build.started)) 44 hdf['started'] = format_datetime(build.started)
40 hdf['started_delta'] = pretty_timedelta(build.started) 45 hdf['started_delta'] = pretty_timedelta(build.started)
41 if build.stopped: 46 if build.stopped:
42 hdf['stopped'] = strftime('%x %X', localtime(build.stopped)) 47 hdf['stopped'] = format_datetime(build.stopped)
43 hdf['stopped_delta'] = pretty_timedelta(build.stopped) 48 hdf['stopped_delta'] = pretty_timedelta(build.stopped)
44 hdf['duration'] = pretty_timedelta(build.stopped, build.started) 49 hdf['duration'] = pretty_timedelta(build.stopped, build.started)
45 hdf['slave'] = { 50 hdf['slave'] = {
46 'name': build.slave, 51 'name': build.slave,
47 'ip_address': build.slave_info.get(Build.IP_ADDRESS), 52 'ipnr': build.slave_info.get(Build.IP_ADDRESS),
48 'os': build.slave_info.get(Build.OS_NAME), 53 'os.name': build.slave_info.get(Build.OS_NAME),
49 'os.family': build.slave_info.get(Build.OS_FAMILY), 54 'os.family': build.slave_info.get(Build.OS_FAMILY),
50 'os.version': build.slave_info.get(Build.OS_VERSION), 55 'os.version': build.slave_info.get(Build.OS_VERSION),
51 'machine': build.slave_info.get(Build.MACHINE), 56 'machine': build.slave_info.get(Build.MACHINE),
52 'processor': build.slave_info.get(Build.PROCESSOR) 57 'processor': build.slave_info.get(Build.PROCESSOR)
53 } 58 }
337 } 342 }
338 req.hdf['page.mode'] = 'overview' 343 req.hdf['page.mode'] = 'overview'
339 req.hdf['config.can_create'] = req.perm.has_permission('BUILD_CREATE') 344 req.hdf['config.can_create'] = req.perm.has_permission('BUILD_CREATE')
340 345
341 def _render_config(self, req, config_name): 346 def _render_config(self, req, config_name):
342 config = BuildConfig.fetch(self.env, config_name) 347 db = self.env.get_db_cnx()
348
349 config = BuildConfig.fetch(self.env, config_name, db=db)
343 req.hdf['title'] = 'Build Configuration "%s"' \ 350 req.hdf['title'] = 'Build Configuration "%s"' \
344 % escape(config.label or config.name) 351 % escape(config.label or config.name)
345 add_link(req, 'up', self.env.href.build(), 'Build Status') 352 add_link(req, 'up', self.env.href.build(), 'Build Status')
346 description = config.description 353 description = config.description
347 if description: 354 if description:
348 description = wiki_to_html(description, self.env, req) 355 description = wiki_to_html(description, self.env, req)
349 req.hdf['config'] = { 356 req.hdf['config'] = {
350 'name': config.name, 'label': config.label, 'path': config.path, 357 'name': config.name, 'label': config.label, 'path': config.path,
358 'min_rev': config.min_rev,
359 'min_rev_href': self.env.href.changeset(config.min_rev),
360 'max_rev': config.max_rev,
361 'max_rev_href': self.env.href.changeset(config.max_rev),
351 'active': config.active, 'description': description, 362 'active': config.active, 'description': description,
352 'browser_href': self.env.href.browser(config.path), 363 'browser_href': self.env.href.browser(config.path),
353 'can_modify': req.perm.has_permission('BUILD_MODIFY'), 364 'can_modify': req.perm.has_permission('BUILD_MODIFY'),
354 'can_delete': req.perm.has_permission('BUILD_DELETE') 365 'can_delete': req.perm.has_permission('BUILD_DELETE')
355 } 366 }
356 req.hdf['page.mode'] = 'view_config' 367 req.hdf['page.mode'] = 'view_config'
357 368
358 platforms = list(TargetPlatform.select(self.env, config=config_name)) 369 platforms = list(TargetPlatform.select(self.env, config=config_name,
370 db=db))
359 req.hdf['config.platforms'] = [ 371 req.hdf['config.platforms'] = [
360 {'name': platform.name, 'id': platform.id} for platform in platforms 372 {'name': platform.name, 'id': platform.id} for platform in platforms
361 ] 373 ]
362 374
363 has_reports = False 375 has_reports = False
364 for report in Report.select(self.env, config=config.name): 376 for report in Report.select(self.env, config=config.name, db=db):
365 has_reports = True 377 has_reports = True
366 break 378 break
367 379
368 if has_reports: 380 if has_reports:
369 req.hdf['config.charts'] = [ 381 req.hdf['config.charts'] = [
390 prefix = 'config.builds.%d' % rev 402 prefix = 'config.builds.%d' % rev
391 req.hdf[prefix + '.href'] = self.env.href.changeset(rev) 403 req.hdf[prefix + '.href'] = self.env.href.changeset(rev)
392 if build and build.status != Build.PENDING: 404 if build and build.status != Build.PENDING:
393 build_hdf = _build_to_hdf(self.env, req, build) 405 build_hdf = _build_to_hdf(self.env, req, build)
394 req.hdf['%s.%s' % (prefix, platform.id)] = build_hdf 406 req.hdf['%s.%s' % (prefix, platform.id)] = build_hdf
407 for step in BuildStep.select(self.env, build=build.id,
408 db=db):
409 req.hdf['%s.%s.steps.%s' % (prefix, platform.id,
410 step.name)] = {
411 'description': escape(step.description),
412 'duration': datetime.fromtimestamp(step.stopped) - \
413 datetime.fromtimestamp(step.started),
414 'failed': not step.successful,
415 'errors': step.errors,
416 'href': build_hdf['href'] + '#step_' + step.name,
417 }
395 idx += 1 418 idx += 1
396 419
397 if page > 1: 420 if page > 1:
398 if page == 2: 421 if page == 2:
399 prev_href = self.env.href.build(config.name) 422 prev_href = self.env.href.build(config.name)
517 'reports': self._render_reports(req, config, build, step) 540 'reports': self._render_reports(req, config, build, step)
518 }) 541 })
519 req.hdf['build.steps'] = steps 542 req.hdf['build.steps'] = steps
520 req.hdf['build.can_delete'] = req.perm.has_permission('BUILD_DELETE') 543 req.hdf['build.can_delete'] = req.perm.has_permission('BUILD_DELETE')
521 544
545 repos = self.env.get_repository(req.authname)
546 chgset = repos.get_changeset(build.rev)
547 req.hdf['build.chgset_author'] = chgset.author
548
522 add_stylesheet(req, 'bitten/bitten.css') 549 add_stylesheet(req, 'bitten/bitten.css')
523 return 'bitten_build.cs', None 550 return 'bitten_build.cs', None
524 551
525 # ITimelineEventProvider methods 552 # ITimelineEventProvider methods
526 553
529 yield ('build', 'Builds') 556 yield ('build', 'Builds')
530 557
531 def get_timeline_events(self, req, start, stop, filters): 558 def get_timeline_events(self, req, start, stop, filters):
532 if 'build' in filters: 559 if 'build' in filters:
533 add_stylesheet(req, 'bitten/bitten.css') 560 add_stylesheet(req, 'bitten/bitten.css')
561
534 db = self.env.get_db_cnx() 562 db = self.env.get_db_cnx()
535 cursor = db.cursor() 563 cursor = db.cursor()
536 cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name," 564 cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,"
537 "b.stopped,b.status FROM bitten_build AS b" 565 "b.stopped,b.status FROM bitten_build AS b"
538 " INNER JOIN bitten_config AS c ON (c.name=b.config)" 566 " INNER JOIN bitten_config AS c ON (c.name=b.config) "
539 " INNER JOIN bitten_platform AS p ON (p.id=b.platform) " 567 " INNER JOIN bitten_platform AS p ON (p.id=b.platform) "
540 "WHERE b.stopped>=%s AND b.stopped<=%s " 568 "WHERE b.stopped>=%s AND b.stopped<=%s "
541 "AND b.status IN (%s, %s) ORDER BY b.stopped", 569 "AND b.status IN (%s, %s) ORDER BY b.stopped",
542 (start, stop, Build.SUCCESS, Build.FAILURE)) 570 (start, stop, Build.SUCCESS, Build.FAILURE))
571
543 event_kinds = {Build.SUCCESS: 'successbuild', 572 event_kinds = {Build.SUCCESS: 'successbuild',
544 Build.FAILURE: 'failedbuild'} 573 Build.FAILURE: 'failedbuild'}
545 for id, config, label, rev, platform, stopped, status in cursor: 574 for id, config, label, rev, platform, stopped, status in cursor:
575
576 errors = []
577 if status == Build.FAILURE:
578 for step in BuildStep.select(self.env, build=id,
579 status=BuildStep.FAILURE,
580 db=db):
581 errors += [(escape(step.name), escape(error)) for error
582 in step.errors]
583
546 title = 'Build of <em>%s [%s]</em> on %s %s' \ 584 title = 'Build of <em>%s [%s]</em> on %s %s' \
547 % (escape(label), escape(rev), escape(platform), 585 % (escape(label), escape(rev), escape(platform),
548 _status_label[status]) 586 _status_label[status])
587 message = ''
549 if req.args.get('format') == 'rss': 588 if req.args.get('format') == 'rss':
550 href = self.env.abs_href.build(config, id) 589 href = self.env.abs_href.build(config, id)
590 if errors:
591 buf = StringIO()
592 prev_step = None
593 for step, error in errors:
594 if step != prev_step:
595 if prev_step is not None:
596 buf.write('</ul>')
597 buf.write('<p>Step %s failed:</p><ul>' % step)
598 prev_step = step
599 buf.write('<li>%s</li>' % escape(error))
600 buf.write('</ul>')
601 message = buf.getvalue()
551 else: 602 else:
552 href = self.env.href.build(config, id) 603 href = self.env.href.build(config, id)
553 yield event_kinds[status], href, title, stopped, None, '' 604 if errors:
605 steps = []
606 for step, error in errors:
607 if step not in steps:
608 steps.append(step)
609 steps = ['<em>%s</em>' % step for step in steps]
610 if len(steps) < 2:
611 message = steps[0]
612 elif len(steps) == 2:
613 message = ' and '.join(steps)
614 elif len(steps) > 2:
615 message = ', '.join(steps[:-1]) + ', and ' + \
616 steps[-1]
617 message = 'Step%s ' % (len(steps) != 1 and 's' or '') \
618 + message + ' failed'
619 yield event_kinds[status], href, title, stopped, None, message
554 620
555 # Internal methods 621 # Internal methods
556 622
557 def _do_invalidate(self, req, build, db): 623 def _do_invalidate(self, req, build, db):
558 self.log.info('Invalidating build %d', build.id) 624 self.log.info('Invalidating build %d', build.id)
Copyright (C) 2012-2017 Edgewall Software