Mercurial > bitten > bitten-test
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) |