view bitten/trac_ext/web_ui.py @ 147:395b67aa072e

Build recipes are now stored in the database with the build configuration. This means that it is no longer necessary to store the recipe in the repository. Closes #41. At some point, there'll need to be a real user interface for creating/updating the recipe.
author cmlenz
date Sun, 21 Aug 2005 17:49:20 +0000
parents 5a27ec93100d
children 4677161d2ae9
line wrap: on
line source
# -*- coding: iso8859-1 -*-
#
# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
#
# Bitten is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Trac is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Author: Christopher Lenz <cmlenz@gmx.de>

import re
from time import localtime, strftime

import pkg_resources
from trac.core import *
from trac.Timeline import ITimelineEventProvider
from trac.util import escape, pretty_timedelta
from trac.web.chrome import INavigationContributor, ITemplateProvider, \
                            add_link, add_stylesheet
from trac.web import IRequestHandler
from trac.wiki import wiki_to_html
from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, BuildLog
from bitten.store import ReportStore
from bitten.trac_ext.api import ILogFormatter

_status_label = {Build.IN_PROGRESS: 'in progress',
                 Build.SUCCESS: 'completed',
                 Build.FAILURE: 'failed'}

def _build_to_hdf(env, req, build):
    hdf = {'id': build.id, 'name': build.slave, 'rev': build.rev,
           'status': _status_label[build.status],
           'cls': _status_label[build.status].replace(' ', '-'),
           'href': env.href.build(build.config, build.id),
           'chgset_href': env.href.changeset(build.rev)}
    if build.started:
        hdf['started'] = strftime('%x %X', localtime(build.started))
        hdf['started_delta'] = pretty_timedelta(build.started)
    if build.stopped:
        hdf['stopped'] = strftime('%x %X', localtime(build.stopped))
        hdf['stopped_delta'] = pretty_timedelta(build.stopped)
        hdf['duration'] = pretty_timedelta(build.stopped, build.started)
    hdf['slave'] = {
        'name': build.slave,
        'ip_address': build.slave_info.get(Build.IP_ADDRESS),
        'os': build.slave_info.get(Build.OS_NAME),
        'os.family': build.slave_info.get(Build.OS_FAMILY),
        'os.version': build.slave_info.get(Build.OS_VERSION),
        'machine': build.slave_info.get(Build.MACHINE),
        'processor': build.slave_info.get(Build.PROCESSOR)
    }
    return hdf

class BittenChrome(Component):
    """Provides the Bitten templates and static resources."""

    implements(ITemplateProvider)

    # ITemplatesProvider methods

    def get_htdocs_dir(self):
        return pkg_resources.resource_filename(__name__, 'htdocs')

    def get_templates_dir(self):
        return pkg_resources.resource_filename(__name__, 'templates')


class BuildConfigController(Component):
    """Implements the web interface for build configurations."""

    implements(INavigationContributor, IRequestHandler)

    # INavigationContributor methods

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

    def get_navigation_items(self, req):
        if not req.perm.has_permission('BUILD_VIEW'):
            return
        yield 'mainnav', 'build', \
              '<a href="%s" accesskey="5">Build Status</a>' \
              % self.env.href.build()

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/build(?:/([\w.-]+))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['config'] = match.group(1)
            return True

    def process_request(self, req):
        req.perm.assert_permission('BUILD_VIEW')

        action = req.args.get('action')
        config = req.args.get('config')

        if req.method == 'POST':
            if config:
                if action == 'new':
                    self._do_create_platform(req, config)
                else:
                    platform_id = req.args.get('platform')
                    if platform_id:
                        if action == 'edit':
                            self._do_save_platform(req, config, platform_id)
                    elif 'delete' in req.args:
                        self._do_delete_platforms(req)
                        self._render_config_form(req, config)
                    elif 'new' in req.args:
                        platform = TargetPlatform(self.env, config=config)
                        self._render_platform_form(req, platform)
                    else:
                        self._do_save_config(req, config)
            else:
                if action == 'new':
                    self._do_create_config(req)
        else:
            if config:
                if action == 'edit':
                    platform_id = req.args.get('platform')
                    if platform_id:
                        platform = TargetPlatform.fetch(self.env,
                                                        int(platform_id))
                        self._render_platform_form(req, platform)
                    elif 'new' in req.args:
                        platform = TargetPlatform(self.env, config=config)
                        self._render_platform_form(req, platform)
                    else:
                        self._render_config_form(req, config)
                else:
                    self._render_config(req, config)
            else:
                if action == 'new':
                    self._render_config_form(req)
                else:
                    self._render_overview(req)

        add_stylesheet(req, 'bitten.css')
        return 'bitten_config.cs', None

    # Internal methods

    def _do_create_config(self, req):
        """Create a new build configuration."""
        req.perm.assert_permission('BUILD_CREATE')

        if 'cancel' in req.args:
            req.redirect(self.env.href.build())

        config_name = req.args.get('name')

        assert not BuildConfig.fetch(self.env, config_name), \
            'A build configuration with the name "%s" already exists' \
            % config_name

        config = BuildConfig(self.env, name=config_name,
                             path=req.args.get('path', ''),
                             recipe=req.args.get('recipe', ''),
                             min_rev=req.args.get('min_rev', ''),
                             max_rev=req.args.get('max_rev', ''),
                             label=req.args.get('label', ''),
                             description=req.args.get('description'))
        config.insert()

        req.redirect(self.env.href.build(config.name))

    def _do_save_config(self, req, config_name):
        """Save changes to a build configuration."""
        req.perm.assert_permission('BUILD_MODIFY')

        if 'cancel' in req.args:
            req.redirect(self.env.href.build(config_name))

        config = BuildConfig.fetch(self.env, config_name)
        assert config, 'Build configuration "%s" does not exist' % config_name

        if 'activate' in req.args:
            config.active = True

        elif 'deactivate' in req.args:
            config.active = False

        else:
            # TODO: Validate recipe, repository path, etc
            config.name = req.args.get('name')
            config.path = req.args.get('path', '')
            config.recipe = req.args.get('recipe', '')
            config.min_rev = req.args.get('min_rev')
            config.max_rev = req.args.get('max_rev')
            config.label = req.args.get('label', '')
            config.description = req.args.get('description', '')

        config.update()
        req.redirect(self.env.href.build(config.name))

    def _do_create_platform(self, req, config_name):
        """Create a new target platform."""
        req.perm.assert_permission('BUILD_MODIFY')

        if 'cancel' in req.args:
            req.redirect(self.env.href.build(config_name, action='edit'))

        platform = TargetPlatform(self.env, config=config_name,
                                  name=req.args.get('name'))

        properties = [int(key[9:]) for key in req.args
                      if key.startswith('property_')]
        properties.sort()
        patterns = [int(key[8:]) for key in req.args
                    if key.startswith('pattern_')]
        patterns.sort()
        platform.rules = [(req.args.get('property_%d' % property),
                           req.args.get('pattern_%d' % pattern))
                          for property, pattern in zip(properties, patterns)]

        add_rules = [int(key[9:]) for key in req.args
                     if key.startswith('add_rule_')]
        if add_rules:
            platform.rules.insert(add_rules[0] + 1, ('', ''))
            self._render_platform_form(req, platform)
            return
        rm_rules = [int(key[8:]) for key in req.args
                     if key.startswith('rm_rule_')]
        if rm_rules:
            del platform.rules[rm_rules[0]]
            self._render_platform_form(req, platform)
            return

        platform.insert()

        req.redirect(self.env.href.build(config_name, action='edit'))

    def _do_delete_platforms(self, req):
        """Delete selected target platforms."""
        req.perm.assert_permission('BUILD_MODIFY')
        self.log.debug('_do_delete_platforms')

        db = self.env.get_db_cnx()
        for platform_id in [int(id) for id in req.args.get('delete_platform')]:
            platform = TargetPlatform.fetch(self.env, platform_id, db=db)
            self.log.info('Deleting target platform %s of configuration %s',
                          platform.name, platform.config)
            platform.delete(db=db)
        db.commit()

    def _do_save_platform(self, req, config_name, platform_id):
        """Save changes to a target platform."""
        req.perm.assert_permission('BUILD_MODIFY')

        if 'cancel' in req.args:
            req.redirect(self.env.href.build(config_name, action='edit'))

        platform = TargetPlatform.fetch(self.env, platform_id)
        platform.name = req.args.get('name')

        properties = [int(key[9:]) for key in req.args
                      if key.startswith('property_')]
        properties.sort()
        patterns = [int(key[8:]) for key in req.args
                    if key.startswith('pattern_')]
        patterns.sort()
        platform.rules = [(req.args.get('property_%d' % property),
                           req.args.get('pattern_%d' % pattern))
                          for property, pattern in zip(properties, patterns)]

        add_rules = [int(key[9:]) for key in req.args
                     if key.startswith('add_rule_')]
        if add_rules:
            platform.rules.insert(add_rules[0] + 1, ('', ''))
            self._render_platform_form(req, platform)
            return
        rm_rules = [int(key[8:]) for key in req.args
                     if key.startswith('rm_rule_')]
        if rm_rules:
            del platform.rules[rm_rules[0]]
            self._render_platform_form(req, platform)
            return

        platform.update()

        req.redirect(self.env.href.build(config_name, action='edit'))

    def _render_overview(self, req):
        req.hdf['title'] = 'Build Status'
        configurations = BuildConfig.select(self.env, include_inactive=True)
        for idx, config in enumerate(configurations):
            description = config.description
            if description:
                description = wiki_to_html(description, self.env, req)
            req.hdf['configs.%d' % idx] = {
                'name': config.name, 'label': config.label or config.name,
                'path': config.path, 'description': description,
                'href': self.env.href.build(config.name),
            }
        req.hdf['page.mode'] = 'overview'
        req.hdf['config.can_create'] = req.perm.has_permission('BUILD_CREATE')

    def _render_config(self, req, config_name):
        config = BuildConfig.fetch(self.env, config_name)
        req.hdf['title'] = 'Build Configuration "%s"' \
                           % escape(config.label or config.name)
        add_link(req, 'up', self.env.href.build(), 'Build Status')
        description = config.description
        if description:
            description = wiki_to_html(description, self.env, req)
        req.hdf['config'] = {
            'name': config.name, 'label': config.label, 'path': config.path,
            'active': config.active, 'description': description,
            'browser_href': self.env.href.browser(config.path),
            'can_modify': req.perm.has_permission('BUILD_MODIFY')
        }
        req.hdf['page.mode'] = 'view_config'

        platforms = TargetPlatform.select(self.env, config=config_name)
        req.hdf['config.platforms'] = [
            {'name': platform.name, 'id': platform.id} for platform in platforms
        ]

        repos = self.env.get_repository(req.authname)
        try:
            root = repos.get_node(config.path)
            for idx, (path, rev, chg) in enumerate(root.get_history()):
                prefix = 'config.builds.%d' % rev
                req.hdf[prefix + '.href'] = self.env.href.changeset(rev)
                for build in Build.select(self.env, config=config.name, rev=rev):
                    if build.status == Build.PENDING:
                        continue
                    req.hdf['%s.%s' % (prefix, build.platform)] = _build_to_hdf(self.env, req, build)
                if idx > 4:
                    break
        except TracError, e:
            self.log.error('Error accessing repository info', exc_info=True)

    def _render_config_form(self, req, config_name=None):
        config = BuildConfig.fetch(self.env, config_name)
        if config:
            req.perm.assert_permission('BUILD_MODIFY')
            req.hdf['config'] = {
                'name': config.name, 'exists': config.exists,
                'path': config.path, 'active': config.active,
                'recipe': config.recipe, 'min_rev': config.min_rev,
                'max_rev': config.max_rev, 'label': config.label,
                'description': config.description
            }

            req.hdf['title'] = 'Edit Build Configuration "%s"' \
                               % escape(config.label or config.name)
            for idx, platform in enumerate(TargetPlatform.select(self.env,
                                                                 config_name)):
                req.hdf['config.platforms.%d' % idx] = {
                    'id': platform.id, 'name': platform.name,
                    'href': self.env.href.build(config_name, action='edit',
                                                platform=platform.id)
                }
        else:
            req.perm.assert_permission('BUILD_CREATE')
            req.hdf['title'] = 'Create Build Configuration'
        req.hdf['page.mode'] = 'edit_config'

    def _render_platform_form(self, req, platform):
        req.perm.assert_permission('BUILD_MODIFY')
        if platform.exists:
            req.hdf['title'] = 'Edit Target Platform "%s"' \
                               % escape(platform.name)
        else:
            req.hdf['title'] = 'Add Target Platform'
        req.hdf['platform'] = {
            'name': platform.name, 'id': platform.id, 'exists': platform.exists,
            'rules': [{'property': propname, 'pattern': pattern}
                      for propname, pattern in platform.rules] or [('', '')]
        }
        req.hdf['page.mode'] = 'edit_platform'


class BuildController(Component):
    """Renders the build page."""
    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider)

    log_formatters = ExtensionPoint(ILogFormatter)

    # INavigationContributor methods

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

    def get_navigation_items(self, req):
        return []

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/build/([\w.-]+)/([\d]+)', req.path_info)
        if match:
            if match.group(1):
                req.args['config'] = match.group(1)
                if match.group(2):
                    req.args['id'] = match.group(2)
            return True

    def process_request(self, req):
        req.perm.assert_permission('BUILD_VIEW')

        db = self.env.get_db_cnx()
        build_id = int(req.args.get('id'))
        build = Build.fetch(self.env, build_id, db=db)
        assert build, 'Build %s does not exist' % build_id

        add_link(req, 'up', self.env.href.build(build.config),
                 'Build Configuration')
        status2title = {Build.SUCCESS: 'Success', Build.FAILURE: 'Failure',
                        Build.IN_PROGRESS: 'In Progress'}
        req.hdf['title'] = 'Build %s - %s' % (build_id,
                                              status2title[build.status])
        req.hdf['page.mode'] = 'view_build'
        config = BuildConfig.fetch(self.env, build.config, db=db)
        req.hdf['build.config'] = {
            'name': config.label,
            'href': self.env.href.build(config.name)
        }

        req.hdf['build'] = _build_to_hdf(self.env, req, build)
        steps = []
        for step in BuildStep.select(self.env, build=build.id, db=db):
            steps.append({
                'name': step.name, 'description': step.description,
                'duration': pretty_timedelta(step.started, step.stopped),
                'failed': step.status == BuildStep.FAILURE
            })
            for log in BuildLog.select(self.env, build=build.id,
                                       step=step.name, db=db):
                formatters = []
                items = []
                for formatter in self.log_formatters:
                    formatters.append(formatter.get_formatter(req, build,
                                                              step,
                                                              log.type))
                for level, message in log.messages:
                    for format in formatters:
                        message = format(level, message)
                    items.append({'level': level, 'message': message})
                steps[-1]['log'] = items

            store = ReportStore(self.env)
            reports = []
            for report in store.retrieve_reports(build, step):
                report_type = report.attr['type']
                report_href = self.env.href.buildreport(build.id, step.name,
                                                        report_type)
                reports.append({'type': report_type, 'href': report_href})
            steps[-1]['reports'] = reports
        req.hdf['build.steps'] = steps

        add_stylesheet(req, 'bitten.css')
        return 'bitten_build.cs', None

    # ITimelineEventProvider methods

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

    def get_timeline_events(self, req, start, stop, filters):
        if 'build' in filters:
            add_stylesheet(req, 'bitten.css')
            db = self.env.get_db_cnx()
            cursor = db.cursor()
            cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,b.slave,"
                           "b.stopped,b.status FROM bitten_build AS b"
                           "  INNER JOIN bitten_config AS c ON (c.name=b.config)"
                           "  INNER JOIN bitten_platform AS p ON (p.id=b.platform) "
                           "WHERE b.stopped>=%s AND b.stopped<=%s "
                           "AND b.status IN (%s, %s) ORDER BY b.stopped",
                           (start, stop, Build.SUCCESS, Build.FAILURE))
            event_kinds = {Build.SUCCESS: 'successbuild',
                           Build.FAILURE: 'failedbuild'}
            for id, config, label, rev, platform, slave, stopped, status in cursor:
                title = 'Build of <em>%s [%s]</em> by %s (%s) %s' \
                        % (escape(label), escape(rev), escape(slave),
                           escape(platform), _status_label[status])
                if req.args.get('format') == 'rss':
                    href = self.env.abs_href.build(config, id)
                else:
                    href = self.env.href.build(config, id)
                yield event_kinds[status], href, title, stopped, None, ''


class SourceFileLinkFormatter(Component):
    """Finds references to files and directories in the repository in the build
    log and renders them as links to the repository browser."""
    
    implements(ILogFormatter)

    def get_formatter(self, req, build, step, type):
        config = BuildConfig.fetch(self.env, build.config)
        repos = self.env.get_repository(req.authname)
        nodes = []
        def _walk(node):
            for child in node.get_entries():
                path = child.path[len(config.path) + 1:]
                pattern = re.compile("([\s'\"])(%s)([\s'\"])" % re.escape(path))
                nodes.append((child.path, pattern))
                if child.isdir:
                    _walk(child)
        _walk(repos.get_node(config.path, build.rev))
        nodes.sort(lambda x, y: -cmp(len(x[0]), len(y[0])))

        def _formatter(level, message):
            for path, pattern in nodes:
                def _replace(m):
                    return '%s<a href="%s">%s</a>%s' % (m.group(1),
                           self.env.href.browser(path, rev=build.rev),
                           m.group(2), m.group(3))
                message = pattern.sub(_replace, message)
            return message
        return _formatter


class BuildReportController(Component):
    """Temporary web interface that simply displays the XML source of a report
    using the Trac `Mimeview` component."""

    implements(INavigationContributor, IRequestHandler)

    template_cs = """<?cs include:"header.cs" ?>
 <div id="ctxtnav" class="nav"></div>
 <div id="content" class="build">
  <h1>Build <a href="<?cs var:build.href ?>"><?cs var:build.id ?></a>: <?cs
    var:report.type ?></h1>
  <?cs var:report.preview ?>
 </div>
<?cs include:"footer.cs" ?>"""

    # INavigationContributor methods

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

    def get_navigation_items(self, req):
        return []

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/buildreport/(?P<build>[\d]+)/(?P<step>[\w]+)'
                         r'/(?P<type>[\w]+)', req.path_info)
        if match:
            for name in match.groupdict():
                req.args[name] = match.group(name)
            return True

    def process_request(self, req):
        req.perm.assert_permission('BUILD_VIEW')

        build = Build.fetch(self.env, int(req.args.get('build')))
        if not build:
            raise TracError, 'Build %d does not exist' % req.args.get('build')
        step = BuildStep.fetch(self.env, build.id, req.args.get('step'))
        if not step:
            raise TracError, 'Build step %s does not exist' \
                             % req.args.get('step')
        report_type = req.args.get('type')

        req.hdf['build'] = {'id': build.id,
                            'href': self.env.href.build(build.config, build.id)}

        store = ReportStore(self.env)
        reports = []
        for report in store.retrieve_reports(build, step, report_type):
            req.hdf['title'] = 'Build %d: %s' % (build.id, report_type)
            xml = report._node.toprettyxml('  ')
            if req.args.get('format') == 'xml':
                req.send_response(200)
                req.send_header('Content-Type', 'text/xml;charset=utf-8')
                req.end_headers()
                req.write(xml)
                return
            else:
                from trac.mimeview import Mimeview
                preview = Mimeview(self.env).render(req, 'application/xml', xml)
                req.hdf['report'] = {'type': report_type, 'preview': preview}
            break

        xml_href = self.env.href.buildreport(build.id, step.name, report_type,
                                             format='xml')
        add_link(req, 'alternate', xml_href, 'XML', 'text/xml')
        add_stylesheet(req, 'css/code.css')
        template = req.hdf.parse(self.template_cs)
        return template, None
Copyright (C) 2012-2017 Edgewall Software