Mercurial > bitten > bitten-test
changeset 41:16b30ffc5fb9
Add web interface for viewing and managing build configurations. Closes #9.
author | cmlenz |
---|---|
date | Thu, 23 Jun 2005 20:33:47 +0000 |
parents | ee31ef783afd |
children | efa525876b1e |
files | bitten/model.py bitten/model/__init__.py bitten/model/build.py bitten/trac_ext/main.py bitten/trac_ext/web_ui.py |
diffstat | 5 files changed, 398 insertions(+), 108 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/bitten/model.py @@ -0,0 +1,207 @@ +# -*- 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> + +from trac.db_default import Table, Column + +schema_version = 1 + + +class Configuration(object): + """Representation of a build configuration.""" + + _table = Table('bitten_config', key='name')[ + Column('name'), Column('path'), Column('label'), + Column('active', type='int'), Column('description') + ] + + def __init__(self, env, name=None, db=None): + self.env = env + self.name = self._old_name = None + self.path = self.label = self.description = self.active = None + if name: + self._fetch(name, db) + + exists = property(fget=lambda self: self._old_name is not None) + + def _fetch(self, name, db=None): + if not db: + db = self.env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT path,label,active,description FROM bitten_config " + "WHERE name=%s", (name,)) + row = cursor.fetchone() + if not row: + raise Exception, "Build configuration %s does not exist" % name + self.name = self._old_name = name + self.path = row[0] or '' + self.label = row[1] or '' + self.active = row[2] and True or False + self.description = row[3] or '' + + def insert(self, db=None): + assert not self.exists, 'Cannot insert existing configuration' + assert self.name, 'Configuration requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_config " + "(name,path,label,active,description) " + "VALUES (%s,%s,%s,%s,%s)", + (self.name, self.path, self.label, int(self.active or 0), + self.description)) + + if handle_ta: + db.commit() + + def update(self, db=None): + assert self.exists, 'Cannot update a non-existing configuration' + assert self.name, 'Configuration requires a name' + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("UPDATE bitten_config SET name=%s,path=%s,label=%s," + "active=%s,description=%s WHERE name=%s", + (self.name, self.path, self.label, int(self.active or 0), + self.description, self._old_name)) + + if handle_ta: + db.commit() + + def select(cls, env, include_inactive=False, db=None): + if not db: + db = env.get_db_cnx() + where = '' + cursor = db.cursor() + if include_inactive: + cursor.execute("SELECT name,path,label,active,description " + "FROM bitten_config ORDER BY name") + else: + cursor.execute("SELECT name,path,label,active,description " + "FROM bitten_config WHERE active=1 " + "ORDER BY name") + for name, path, label, active, description in cursor: + config = Configuration(env) + config.name = name + config.path = path or '' + config.label = label or '' + config.active = active and True or False + config.description = description or '' + yield config + select = classmethod(select) + + +class Build(object): + """Representation of a build.""" + + _table = Table('bitten_build', key=('path', 'rev', 'slave'))[ + Column('rev'), Column('path'), Column('slave'), + Column('time', type='int'), Column('duration', type='int'), + Column('status', type='int') + ] + + FAILURE = 'failure' + IN_PROGRESS = 'in-progress' + SUCCESS = 'success' + + def __init__(self, env, path=None, rev=None, slave=None, db=None): + self.env = env + self.rev = self.path = self.slave = None + self.time = self.duration = self.status = None + if rev: + self._fetch(rev, path, slave, db) + + def _fetch(self, rev, path, slave, db=None): + if not db: + db = self.env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT time,duration,status FROM bitten_build " + "WHERE rev=%s AND path=%s AND slave=%s", + (rev, path, slave)) + row = cursor.fetchone() + if not row: + raise Exception, "Build not found" + self.rev = rev + self.path = path + self.slave = slave + self.time = row[0] and int(row[0]) + self.duration = row[1] and int(row[1]) + if row[2] is not None: + self.status = row[2] and Build.SUCCESS or Build.FAILURE + else: + self.status = Build.IN_PROGRESS + + completed = property(fget=lambda self: self.status != Build.IN_PROGRESS) + successful = property(fget=lambda self: self.status == Build.SUCCESS) + + def insert(self, db=None): + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + cursor = db.cursor() + cursor.execute("INSERT INTO bitten_build VALUES (%s,%s,%s,%s,%s,%s)", + (self.rev, self.path, self.slave, self.time, + self.duration, self.status or Build.IN_PROGRESS)) + + def select(cls, env, path=None, rev=None, slave=None, db=None): + if not db: + db = env.get_db_cnx() + + where_clauses = [] + if rev is not None: + where_clauses.append(("rev=%s", rev)) + if path is not None: + where_clauses.append(("path=%s", path)) + if slave is not None: + where_clauses.append(("slave=%s", path)) + if where_clauses: + where = "WHERE " + " AND ".join([wc[0] for wc in where_clauses]) + else: + where = "" + + cursor = db.cursor() + cursor.execute("SELECT rev,path,slave,time,duration,status " + "FROM bitten_build " + where, + [wc[1] for wc in where_clauses]) + for rev, path, slave, time, duration, status in cursor: + build = Build(env) + build.rev = rev + build.path = path + build.slave = slave + build.time = time and int(time) + build.duration = duration and int(duration) + if status is not None: + build.status = status and Build.SUCCESS or Build.FAILURE + else: + build.status = Build.FAILURE + yield build + select = classmethod(select)
deleted file mode 100644 --- a/bitten/model/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- 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> - -from bitten.model.build import * - -schema_version = 1
deleted file mode 100644 --- a/bitten/model/build.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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> - -from trac.db_default import Table, Column - - -class Build(object): - """Representation of a build.""" - - _table = Table('bitten_build', key=('rev', 'path', 'slave'))[ - Column('rev'), Column('path'), Column('slave'), - Column('time', type='int'), Column('duration', type='int'), - Column('status', type='int') - ] - - FAILURE = 'failure' - IN_PROGRESS = 'in-progress' - SUCCESS = 'success' - - def __init__(self, env, rev=None, path=None, slave=None, db=None): - self.env = env - self.rev = self.path = self.slave = None - self.time = self.duration = self.status = None - if rev: - self._fetch(rev, path, slave, db) - - def _fetch(self, rev, path, slave, db=None): - if not db: - db = self.env.get_db_cnx() - - cursor = db.cursor() - cursor.execute("SELECT time,duration,status FROM bitten_build " - "WHERE rev=%s AND path=%s AND slave=%s", - (rev, path, slave)) - row = cursor.fetchone() - if not row: - raise Exception, "Build not found" - self.time = row[0] and int(row[0]) - self.duration = row[1] and int(row[1]) - if row[2] is not None: - self.status = row[2] and Build.SUCCESS or Build.FAILURE - else: - self.status = Build.FAILURE - - completed = property(fget=lambda self: self.status != Build.IN_PROGRESS) - successful = property(fget=lambda self: self.status == Build.SUCCESS) - - def insert(self, db=None): - if not db: - db = self.env.get_db_cnx() - handle_ta = True - else: - handle_ta = False - - cursor = db.cursor() - cursor.execute("INSERT INTO bitten_build VALUES (%s,%s,%s,%s,%s,%s)", - (self.rev, self.path, self.slave, self.time, - self.duration, self.status or Build.IN_PROGRESS))
--- a/bitten/trac_ext/main.py +++ b/bitten/trac_ext/main.py @@ -22,7 +22,7 @@ from trac.core import * from trac.env import IEnvironmentSetupParticipant -from bitten.model import Build, schema_version +from bitten.model import Build, Configuration, schema_version from bitten.trac_ext import web_ui class BuildSystem(Component): @@ -35,7 +35,7 @@ # Create the required tables db = self.env.get_db_cnx() cursor = db.cursor() - for table in [Build._table]: + for table in [Build._table, Configuration._table]: cursor.execute(db.to_sql(table)) tarballs_dir = os.path.join(self.env.path, 'snapshots') @@ -57,3 +57,19 @@ row = cursor.fetchone() if not row: self.environment_created() + else: + current_version = int(row.fetchone()[0]) + for i in range(current_version + 1, schema_version + 1): + name = 'db%i' % i + try: + upgrades = __import__('upgrades', globals(), locals(), + [name]) + script = getattr(upgrades, name) + except AttributeError: + err = 'No upgrade module for version %i (%s.py)' % (i, name) + raise TracError, err + script.do_upgrade(self.env, i, cursor) + cursor.execute("UPDATE system SET value=%s WHERE " + "name='bitten_version'", (schema_version)) + self.log.info('Upgraded Bitten tables from version %d to %d', + current_version, schema_version)
--- a/bitten/trac_ext/web_ui.py +++ b/bitten/trac_ext/web_ui.py @@ -21,9 +21,11 @@ import re from trac.core import * +from trac.util import escape from trac.web.chrome import INavigationContributor from trac.web.main import IRequestHandler - +from trac.wiki import wiki_to_html +from bitten.model import Configuration class BuildModule(Component): @@ -32,9 +34,71 @@ build_cs = """ <?cs include:"header.cs" ?> <div id="ctxtnav" class="nav"></div> - <div id="content"> - <h1>Build Status</h1> - <p>Not yet implemented</p> + <div id="content" class="build"> + <h1><?cs var:title ?></h1><?cs + + if:build.mode == 'overview' ?><?cs + each:config = build.configs ?> + <h2><a href="<?cs var:config.href ?>"><?cs var:config.label ?></a></h2><?cs + if:config.description ?><div class="description"><?cs + var:config.description ?></div><?cs + /if ?><?cs + /each ?> + <div class="buttons"> + <form method="get" action=""><div> + <input type="hidden" name="action" value="new" /> + <input type="submit" value="Add new configuration" /> + </div></form> + </div><?cs + + elif:build.mode == 'edit_config' ?> + <form method="post" action=""> + <div class="field"><label>Name:<br /> + <input type="text" name="name" value="<?cs var:build.config.name ?>" /> + </label></div> + <div class="field"><label>Label (for display):<br /> + <input type="text" name="label" size="32" value="<?cs + var:build.config.label ?>" /> + </label></div> + <div class="field"><label><input type="checkbox" name="active"<?cs + if:build.config.active ?> checked="checked" <?cs /if ?>/> Active + </label></div> + <div class="field"><label>Repository path:<br /> + <input type="text" name="path" size="48" value="<?cs + var:build.config.path ?>" /> + </label></div> + <div class="field"><fieldset class="iefix"> + <label for="description">Description (you may use <a tabindex="42" href="<?cs + var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label> + <p><textarea id="description" name="description" class="wikitext" rows="10" cols="78"><?cs + var:build.config.description ?></textarea></p> + <script type="text/javascript" src="<?cs + var:htdocs_location ?>js/wikitoolbar.js"></script> + </fieldset></div> + <div class="buttons"> + <input type="hidden" name="action" value="<?cs + if:build.config.exists ?>edit<?cs else ?>new<?cs /if ?>" /> + <input type="submit" name="cancel" value="Cancel" /> + <input type="submit" value="<?cs + if:build.config.exists ?>Save changes<?cs else ?>Create<?cs /if ?>" /> + </div> + </form><?cs + + elif:build.mode == 'view_config' ?><ul> + <li>Active: <?cs if:build.config.active ?>yes<?cs else ?>no<?cs /if ?></li> + <li>Path: <?cs if:build.config.path ?><a href="<?cs + var:build.config.browser_href ?>"><?cs + var:build.config.path ?></a></li><?cs /if ?></ul> + <?cs if:build.config.description ?><div class="description"><?cs + var:build.config.description ?></div><?cs /if ?> + <div class="buttons"> + <form method="get" action=""><div> + <input type="hidden" name="action" value="edit" /> + <input type="submit" value="Edit configuration" /> + </div></form> + </div><?cs + /if ?> + </div> <?cs include:"footer.cs" ?> """ @@ -45,17 +109,118 @@ return 'build' def get_navigation_items(self, req): - yield 'mainnav', 'build', '<a href="%s">Build Status</a>' \ - % self.env.href.build() + 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) + match = re.match(r'/build(?:/(\w+))?(?:/(\w+))?', req.path_info) if match: if match.group(1): - req.args['id'] = 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): + action = req.args.get('action') + config = req.args.get('config') + id = req.args.get('id') + + if req.method == 'POST': + if not config and action == 'new': + self._do_create(req) + elif config and action == 'edit': + self._do_save(req, config) + else: + if not config: + if action == 'new': + self._render_config_form(req) + else: + self._render_overview(req) + elif not id: + if action == 'edit': + self._render_config_form(req, config) + else: + self._render_config(req, config) + else: + self._render_build(req, config, id) + return req.hdf.parse(self.build_cs), None + + def _do_create(self, req): + """Create a new build configuration.""" + if 'cancel' in req.args.keys(): + req.redirect(self.env.href.build()) + + config = Configuration(self.env) + config.name = req.args.get('name') + config.active = req.args.has_key('active') + config.label = req.args.get('label', '') + config.path = req.args.get('path', '') + config.description = req.args.get('description', '') + config.insert() + + req.redirect(self.env.href.build(config.name)) + + def _do_save(self, req, config_name): + """Save changes to a build configuration.""" + if 'cancel' in req.args.keys(): + req.redirect(self.env.href.build(config_name)) + + config = Configuration(self.env, config_name) + config.name = req.args.get('name') + config.active = req.args.has_key('active') + config.label = req.args.get('label', '') + config.path = req.args.get('path', '') + config.description = req.args.get('description', '') + config.update() + + req.redirect(self.env.href.build(config.name)) + + def _render_overview(self, req): + req.hdf['title'] = 'Build Status' + configurations = Configuration.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['build.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['build.mode'] = 'overview' + + def _render_config(self, req, config_name): + config = Configuration(self.env, config_name) + req.hdf['title'] = 'Build Configuration "%s"' \ + % escape(config.label or config.name) + description = config.description + if description: + description = wiki_to_html(description, self.env, req) + req.hdf['build.config'] = { + 'name': config.name, 'label': config.label, 'path': config.path, + 'active': config.active, 'description': description, + 'browser_href': self.env.href.browser(config.path) + } + req.hdf['build.mode'] = 'view_config' + + def _render_config_form(self, req, config_name=None): + config = Configuration(self.env, config_name) + if config.exists: + req.hdf['title'] = 'Edit Build Configuration "%s"' \ + % escape(config.label or config.name) + req.hdf['build.config'] = { + 'name': config.name, 'label': config.label, 'path': config.path, + 'active': config.active, 'description': config.description, + 'exists': config.exists + } + else: + req.hdf['title'] = 'Create Build Configuration' + req.hdf['build.mode'] = 'edit_config' + + def _render_build(self, req, config_name, build_id): + raise NotImplementedError, 'Not implemented yet'