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'
Copyright (C) 2012-2017 Edgewall Software