changeset 96:c8c36f34ff5a

Change model class pattern: * Using normal constructor creates a new object, that can added to the database using the {{{insert()}}} method. * For getting an existing object, the class method {{{fetch()}}} must be used. It's similar to {{{select()}}}, but returns only one object.
author cmlenz
date Sun, 17 Jul 2005 20:14:43 +0000
parents 1984b2e01998
children 03c8b5e3f111
files bitten/master.py bitten/model.py bitten/tests/model.py bitten/trac_ext/__init__.py bitten/trac_ext/main.py bitten/trac_ext/tests/web_ui.py bitten/trac_ext/web_ui.py
diffstat 7 files changed, 229 insertions(+), 239 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -130,7 +130,7 @@
     def get_snapshot(self, build, format):
         snapshot = self.snapshots.get((build.config, build.rev, format))
         if not snapshot:
-            config = BuildConfig(self.env, build.config)
+            config = BuildConfig.fetch(self.env, build.config)
             snapshot = archive.pack(self.env, path=config.path, rev=build.rev,
                                     prefix=config.name, format=format)
             log.info('Prepared snapshot archive at %s' % snapshot)
@@ -155,8 +155,8 @@
                         match = False
                         break
                 if match:
-                    log.info('Slave %s matched target platform %s',
-                             handler.name, platform.name)
+                    log.debug('Slave %s matched target platform %s',
+                              handler.name, platform.name)
                     self.slaves[platform.id].add(handler)
                     any_match = True
 
@@ -285,18 +285,17 @@
                     build.slave_info.update(self.info)
                     build.started = int(_parse_iso_datetime(elem.attr['time']))
                     build.status = Build.IN_PROGRESS
-                    log.info('Slave %s started build of "%s" as of [%s]',
-                             self.name, build.config, build.rev)
+                    log.info('Slave %s started build %d ("%s" as of [%s])',
+                             self.name, build.id, build.config, build.rev)
 
                 elif elem.name == 'step':
                     log.info('Slave completed step "%s"', elem.attr['id'])
-                    step = BuildStep(self.env)
-                    step.build = build.id
-                    step.name = elem.attr['id']
-                    step.description = elem.attr.get('description')
+                    step = BuildStep(self.env, build=build.id,
+                                     name=elem.attr['id'],
+                                     description=elem.attr.get('description'),
+                                     log=elem.gettext().strip())
                     step.started = int(_parse_iso_datetime(elem.attr['time']))
                     step.stopped = step.started + int(elem.attr['duration'])
-                    step.log = elem.gettext().strip()
                     if elem.attr['result'] == 'failure':
                         log.warning('Step failed: %s', elem.gettext())
                         step.status = BuildStep.FAILURE
@@ -305,8 +304,8 @@
                     step.insert(db=db)
 
                 elif elem.name == 'completed':
-                    log.info('Slave %s completed build of "%s" as of [%s]',
-                             self.name, build.config, build.rev)
+                    log.info('Slave %s completed build %d ("%s" as of [%s])',
+                             self.name, build.id, build.config, build.rev)
                     build.stopped = int(_parse_iso_datetime(elem.attr['time']))
                     if elem.attr['result'] == 'failure':
                         build.status = Build.FAILURE
@@ -314,7 +313,8 @@
                         build.status = Build.SUCCESS
 
                 elif elem.name == 'aborted':
-                    log.info('Slave "%s" aborted build %d', self.name, build.id)
+                    log.info('Slave "%s" aborted build %d ("%s" as of [%s])',
+                             self.name, build.id, build.config, build.rev)
                     build.slave = None
                     build.started = 0
                     build.status = Build.PENDING
--- a/bitten/model.py
+++ b/bitten/model.py
@@ -31,31 +31,18 @@
         ]
     ]
 
-    def __init__(self, env, name=None, db=None):
+    def __init__(self, env, name=None, path=None, label=None, active=False,
+                 description=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)
+        self._old_name = None
+        self.name = name
+        self.path = path or ''
+        self.label = label or ''
+        self.active = bool(active)
+        self.description = description or ''
 
     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'
@@ -93,6 +80,27 @@
         if handle_ta:
             db.commit()
 
+    def fetch(cls, env, name, db=None):
+        if not db:
+            db = 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:
+            return None
+
+        config = BuildConfig(env)
+        config.name = config._old_name = name
+        config.path = row[0] or ''
+        config.label = row[1] or ''
+        config.active = row[2] and True or False
+        config.description = row[3] or ''
+        return config
+
+    fetch = classmethod(fetch)
+
     def select(cls, env, include_inactive=False, db=None):
         if not db:
             db = env.get_db_cnx()
@@ -106,13 +114,10 @@
                            "FROM bitten_config WHERE active=1 "
                            "ORDER BY name")
         for name, path, label, active, description in cursor:
-            config = BuildConfig(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
+            yield BuildConfig(env, name=name, path=path or '',
+                              label=label or '', active=bool(active),
+                              description=description or '')
+
     select = classmethod(select)
 
 
@@ -129,32 +134,12 @@
         ]
     ]
 
-    def __init__(self, env, id=None, db=None):
+    def __init__(self, env, id=None, config=None, name=None, db=None):
         self.env = env
+        self.id = id
+        self.config = config
+        self.name = name
         self.rules = []
-        if id is not None:
-            self._fetch(id, db)
-        else:
-            self.id = self.config = self.name = None
-
-    def _fetch(self, id, db=None):
-        if not db:
-            db = self.env.get_db_cnx()
-
-        cursor = db.cursor()
-        cursor.execute("SELECT config,name FROM bitten_platform "
-                       "WHERE id=%s", (id,))
-        row = cursor.fetchone()
-        if not row:
-            raise Exception, 'Target platform %s does not exist' % id
-        self.id = id
-        self.config = row[0]
-        self.name = row[1]
-
-        cursor.execute("SELECT propname,pattern FROM bitten_rule "
-                       "WHERE id=%s ORDER BY orderno", (id,))
-        for propname, pattern in cursor:
-            self.rules.append((propname, pattern))
 
     exists = property(fget=lambda self: self.id is not None)
 
@@ -216,6 +201,26 @@
         if handle_ta:
             db.commit()
 
+    def fetch(cls, env, id, db=None):
+        if not db:
+            db = env.get_db_cnx()
+
+        cursor = db.cursor()
+        cursor.execute("SELECT config,name FROM bitten_platform "
+                       "WHERE id=%s", (id,))
+        row = cursor.fetchone()
+        if not row:
+            return None
+
+        platform = TargetPlatform(env, id=id, config=row[0], name=row[1])
+        cursor.execute("SELECT propname,pattern FROM bitten_rule "
+                       "WHERE id=%s ORDER BY orderno", (id,))
+        for propname, pattern in cursor:
+            platform.rules.append((propname, pattern))
+        return platform
+
+    fetch = classmethod(fetch)
+
     def select(cls, env, config=None, db=None):
         if not db:
             db = env.get_db_cnx()
@@ -232,7 +237,8 @@
         cursor.execute("SELECT id FROM bitten_platform %s ORDER BY name"
                        % where, [wc[1] for wc in where_clauses])
         for (id,) in cursor:
-            yield TargetPlatform(env, id)
+            yield TargetPlatform.fetch(env, id)
+
     select = classmethod(select)
 
 
@@ -267,40 +273,20 @@
     MACHINE = 'machine'
     PROCESSOR = 'processor'
 
-    def __init__(self, env, id=None, db=None):
+    def __init__(self, env, id=None, config=None, rev=None, platform=None,
+                 slave=None, started=0, stopped=0, rev_time=0,
+                 status=PENDING):
         self.env = env
         self.slave_info = {}
-        if id is not None:
-            self._fetch(id, db)
-        else:
-            self.id = self.config = self.rev = self.platform = self.slave = None
-            self.started = self.stopped = self.rev_time = 0
-            self.status = self.PENDING
-
-    def _fetch(self, id, db=None):
-        if not db:
-            db = self.env.get_db_cnx()
-
-        cursor = db.cursor()
-        cursor.execute("SELECT config,rev,rev_time,platform,slave,started,"
-                       "stopped,status FROM bitten_build WHERE id=%s", (id,))
-        row = cursor.fetchone()
-        if not row:
-            raise Exception, "Build %s not found" % id
         self.id = id
-        self.config = row[0]
-        self.rev = row[1]
-        self.rev_time = int(row[2])
-        self.platform = int(row[3])
-        self.slave = row[4]
-        self.started = row[5] and int(row[5])
-        self.stopped = row[6] and int(row[6])
-        self.status = row[7]
-
-        cursor.execute("SELECT propname,propvalue FROM bitten_slave "
-                       "WHERE build=%s", (self.id,))
-        for propname, propvalue in cursor:
-            self.slave_info[propname] = propvalue
+        self.config = config
+        self.rev = rev
+        self.platform = platform
+        self.slave = slave
+        self.started = started or 0
+        self.stopped = stopped or 0
+        self.rev_time = rev_time
+        self.status = status
 
     exists = property(fget=lambda self: self.id is not None)
     completed = property(fget=lambda self: self.status != Build.IN_PROGRESS)
@@ -373,6 +359,29 @@
         if handle_ta:
             db.commit()
 
+    def fetch(cls, env, id, db=None):
+        if not db:
+            db = env.get_db_cnx()
+
+        cursor = db.cursor()
+        cursor.execute("SELECT config,rev,rev_time,platform,slave,started,"
+                       "stopped,status FROM bitten_build WHERE id=%s", (id,))
+        row = cursor.fetchone()
+        if not row:
+            return None
+
+        build = Build(env, id=id, config=row[0], rev=row[1],
+                      rev_time=int(row[2]), platform=int(row[3]),
+                      slave=row[4], started=row[5] and int(row[5]) or 0,
+                      stopped=row[6] and int(row[6]) or 0, status=row[7])
+        cursor.execute("SELECT propname,propvalue FROM bitten_slave "
+                       "WHERE build=%s", (id,))
+        for propname, propvalue in cursor:
+            build.slave_info[propname] = propvalue
+        return build
+
+    fetch = classmethod(fetch)
+
     def select(cls, env, config=None, rev=None, platform=None, slave=None,
                status=None, db=None):
         if not db:
@@ -395,23 +404,11 @@
             where = ""
 
         cursor = db.cursor()
-        cursor.execute("SELECT id,config,rev,platform,slave,started,stopped,"
-                       "status,rev_time FROM bitten_build %s "
+        cursor.execute("SELECT id FROM bitten_build %s "
                        "ORDER BY config,rev_time DESC,slave"
                        % where, [wc[1] for wc in where_clauses])
-        for id, config, rev, platform, slave, started, stopped, status, \
-                rev_time in cursor:
-            build = Build(env)
-            build.id = id
-            build.config = config
-            build.rev = rev
-            build.platform = platform
-            build.slave = slave
-            build.started = started and int(started) or 0
-            build.stopped = stopped and int(stopped) or 0
-            build.status = status
-            build.rev_time = int(rev_time)
-            yield build
+        for (id,) in cursor:
+            yield Build.fetch(env, id)
     select = classmethod(select)
 
 
@@ -430,32 +427,16 @@
     SUCCESS = 'S'
     FAILURE = 'F'
 
-    def __init__(self, env, build=None, name=None, db=None):
+    def __init__(self, env, build=None, name=None, description=None,
+                 status=None, log=None, started=None, stopped=None):
         self.env = env
-        if build is not None and name is not None:
-            self._fetch(build, name, db)
-        else:
-            self.build = self.name = self.description = self.status = None
-            self.log = self.started = self.stopped = None
-
-    def _fetch(self, build, name, db=None):
-        if not db:
-            db = self.env.get_db_cnx()
-
-        cursor = db.cursor()
-        cursor.execute("SELECT description,status,log,started,stopped "
-                       "FROM bitten_step WHERE build=%s AND name=%s",
-                       (build, name))
-        row = cursor.fetchone()
-        if not row:
-            raise Exception, "Build step %s of %s not found" % (name, build)
         self.build = build
         self.name = name
-        self.description = row[0] or ''
-        self.status = row[1]
-        self.log = row[2] or ''
-        self.started = row[3] and int(row[3]) or 0
-        self.stopped = row[4] and int(row[4]) or 0
+        self.description = description
+        self.status = status
+        self.log = log
+        self.started = started
+        self.stopped = stopped
 
     exists = property(fget=lambda self: self.build is not None)
     successful = property(fget=lambda self: self.status == BuildStep.SUCCESS)
@@ -492,6 +473,22 @@
         if handle_ta:
             db.commit()
 
+    def fetch(cls, env, build, name, db=None):
+        if not db:
+            db = env.get_db_cnx()
+
+        cursor = db.cursor()
+        cursor.execute("SELECT description,status,log,started,stopped "
+                       "FROM bitten_step WHERE build=%s AND name=%s",
+                       (build, name))
+        row = cursor.fetchone()
+        if not row:
+            return None
+
+        return BuildStep(env, build, name, row[0] or '', row[1], row[2] or '',
+                         row[3] and int(row[3]), row[4] and int(row[4]))
+    fetch = classmethod(fetch)
+
     def select(cls, env, build=None, name=None, db=None):
         if not db:
             db = env.get_db_cnx()
@@ -511,15 +508,9 @@
                        "stopped FROM bitten_step %s ORDER BY stopped"
                        % where, [wc[1] for wc in where_clauses])
         for build, name, description, status, log, started, stopped in cursor:
-            step = BuildStep(env)
-            step.build = build
-            step.name = name
-            step.description = description
-            step.status = status
-            step.log = log
-            step.started = started and int(started) or 0
-            step.stopped = stopped and int(stopped) or 0
-            yield step
+            yield BuildStep(env, build, name, description or '', status,
+                            log or '', started and int(started),
+                            stopped and int(stopped))
     select = classmethod(select)
 
 
--- a/bitten/tests/model.py
+++ b/bitten/tests/model.py
@@ -35,14 +35,23 @@
         db.commit()
 
     def test_new_config(self):
-        config = BuildConfig(self.env)
+        config = BuildConfig(self.env, name='test')
         assert not config.exists
 
+    def test_fetch(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("INSERT INTO bitten_config (name,path,label,active) "
+                       "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0))
+        config = BuildConfig.fetch(self.env, name='test')
+        assert config.exists
+        self.assertEqual('test', config.name)
+        self.assertEqual('trunk', config.path)
+        self.assertEqual('Test', config.label)
+        self.assertEqual(False, config.active)
+
     def test_insert_config(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.label = 'Test'
-        config.path = 'trunk'
+        config = BuildConfig(self.env, name='test', path='trunk', label='Test')
         config.insert()
 
         db = self.env.get_db_cnx()
@@ -72,11 +81,8 @@
         self.assertEqual([], platform.rules)
 
     def test_insert(self):
-        platform = TargetPlatform(self.env)
-        platform.config = 'test'
-        platform.name = 'Windows XP'
-        platform.rules.append((Build.OS_NAME, 'Windows'))
-        platform.rules.append((Build.OS_VERSION, 'XP'))
+        platform = TargetPlatform(self.env, config='test', name='Windows XP')
+        platform.rules += [(Build.OS_NAME, 'Windows'), (Build.OS_VERSION, 'XP')]
         platform.insert()
 
         assert platform.exists
@@ -97,7 +103,7 @@
         cursor.execute("INSERT INTO bitten_platform (config,name) "
                        "VALUES (%s,%s)", ('test', 'Windows'))
         id = db.get_last_id('bitten_platform')
-        platform = TargetPlatform(self.env, id)
+        platform = TargetPlatform.fetch(self.env, id)
         assert platform.exists
         self.assertEqual('test', platform.config)
         self.assertEqual('Windows', platform.name)
@@ -130,13 +136,10 @@
         self.assertEqual(0, build.started)
 
     def test_insert(self):
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev = '42'
-        build.rev_time = 12039
-        build.platform = 1
-        build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'
-        build.slave_info[Build.MAINTAINER] = 'joe@example.org'
+        build = Build(self.env, config='test', rev='42', rev_time=12039,
+                      platform=1)
+        build.slave_info.update({Build.IP_ADDRESS: '127.0.0.1',
+                                 Build.MAINTAINER: 'joe@example.org'})
         build.insert()
 
         db = self.env.get_db_cnx()
@@ -155,40 +158,21 @@
         build = Build(self.env)
         self.assertRaises(AssertionError, build.insert)
 
-        # No config
-        build = Build(self.env)
-        build.rev = '42'
-        build.rev_time = 12039
-        build.platform = 1
-        self.assertRaises(AssertionError, build.insert)
+        build = Build(self.env, rev='42', rev_time=12039, platform=1)
+        self.assertRaises(AssertionError, build.insert) # No config
 
-        # No rev
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev_time = 12039
-        build.platform = 1
-        self.assertRaises(AssertionError, build.insert)
+        build = Build(self.env, config='test', rev_time=12039, platform=1)
+        self.assertRaises(AssertionError, build.insert) # No rev
 
-        # No rev time
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev = '42'
-        build.platform = 1
-        self.assertRaises(AssertionError, build.insert)
+        build = Build(self.env, config='test', rev='42', platform=1)
+        self.assertRaises(AssertionError, build.insert) # No rev time
 
-        # No platform
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev = '42'
-        build.rev_time = 12039
-        self.assertRaises(AssertionError, build.insert)
+        build = Build(self.env, config='test', rev='42', rev_time=12039)
+        self.assertRaises(AssertionError, build.insert) # No platform
 
     def test_insert_no_slave(self):
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev = '42'
-        build.rev_time = 12039
-        build.platform = 1
+        build = Build(self.env, config='test', rev='42', rev_time=12039,
+                      platform=1)
         build.status = Build.SUCCESS
         self.assertRaises(AssertionError, build.insert)
         build.status = Build.FAILURE
@@ -199,11 +183,8 @@
         build.insert()
 
     def test_insert_invalid_status(self):
-        build = Build(self.env)
-        build.config = 'test'
-        build.rev = '42'
-        build.rev_time = 12039
-        build.status = 'DUNNO'
+        build = Build(self.env, config='test', rev='42', rev_time=12039,
+                      status='DUNNO')
         self.assertRaises(AssertionError, build.insert)
 
     def test_fetch(self):
@@ -219,7 +200,7 @@
                            [(build_id, Build.IP_ADDRESS, '127.0.0.1'),
                             (build_id, Build.MAINTAINER, 'joe@example.org')])
 
-        build = Build(self.env, build_id)
+        build = Build.fetch(self.env, build_id)
         self.assertEquals(build_id, build.id)
         self.assertEquals('127.0.0.1', build.slave_info[Build.IP_ADDRESS])
         self.assertEquals('joe@example.org', build.slave_info[Build.MAINTAINER])
@@ -241,11 +222,8 @@
         self.assertEqual(None, step.name)
 
     def test_insert(self):
-        step = BuildStep(self.env)
-        step.build = 1
-        step.name = 'test'
-        step.description = 'Foo bar'
-        step.status = BuildStep.SUCCESS
+        step = BuildStep(self.env, build=1, name='test', description='Foo bar',
+                         status=BuildStep.SUCCESS)
         step.insert()
 
         db = self.env.get_db_cnx()
@@ -256,15 +234,11 @@
                          cursor.fetchone())
 
     def test_insert_no_build_or_name(self):
-        # No build
-        step = BuildStep(self.env)
-        step.name = 'test'
-        self.assertRaises(AssertionError, step.insert)
+        step = BuildStep(self.env, name='test')
+        self.assertRaises(AssertionError, step.insert) # No build
 
-        # No name
-        step = BuildStep(self.env)
-        step.build = 1
-        self.assertRaises(AssertionError, step.insert)
+        step = BuildStep(self.env, build=1)
+        self.assertRaises(AssertionError, step.insert) # No name
 
     def test_fetch(self):
         db = self.env.get_db_cnx()
@@ -272,7 +246,7 @@
         cursor.execute("INSERT INTO bitten_step VALUES (%s,%s,%s,%s,%s,%s,%s)",
                        (1, 'test', 'Foo bar', BuildStep.SUCCESS, '', 0, 0))
 
-        step = BuildStep(self.env, 1, 'test')
+        step = BuildStep.fetch(self.env, build=1, name='test')
         self.assertEqual(1, step.build)
         self.assertEqual('test', step.name)
         self.assertEqual('Foo bar', step.description)
--- a/bitten/trac_ext/__init__.py
+++ b/bitten/trac_ext/__init__.py
@@ -17,3 +17,6 @@
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
+
+import bitten.trac_ext.main
+import bitten.trac_ext.web_ui
--- a/bitten/trac_ext/main.py
+++ b/bitten/trac_ext/main.py
@@ -23,12 +23,15 @@
 from trac.core import *
 from trac.env import IEnvironmentSetupParticipant
 from trac.perm import IPermissionRequestor
-from bitten.model import schema, schema_version
+from trac.wiki import IWikiSyntaxProvider
+from bitten.model import schema, schema_version, Build, BuildConfig
 from bitten.trac_ext import web_ui
 
+
 class BuildSystem(Component):
 
-    implements(IEnvironmentSetupParticipant, IPermissionRequestor)
+    implements(IEnvironmentSetupParticipant, IPermissionRequestor,
+               IWikiSyntaxProvider)
 
     # IEnvironmentSetupParticipant methods
 
@@ -80,3 +83,21 @@
     def get_permission_actions(self):
         actions = ['BUILD_VIEW', 'BUILD_CREATE', 'BUILD_MODIFY', 'BUILD_DELETE']
         return actions + [('BUILD_ADMIN', actions)]
+
+    # IWikiSyntaxProvider methods
+
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        def _format_link(formatter, ns, name, label):
+            build = Build.fetch(self.env, int(name))
+            if build:
+                config = BuildConfig.fetch(self.env, build.config)
+                title = 'Build %d ([%s] of %s) by %s' % (build.id, build.rev,
+                        config.label, build.slave)
+                return '<a class="build" href="%s" title="%s">%s</a>' \
+                       % (formatter.href.build(build.config, build.id), title,
+                          label)
+            return label
+        yield 'build', _format_link
--- a/bitten/trac_ext/tests/web_ui.py
+++ b/bitten/trac_ext/tests/web_ui.py
@@ -118,7 +118,7 @@
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test', redirected_to[0])
 
-        build = BuildConfig(self.env, 'test')
+        build = BuildConfig.fetch(self.env, 'test')
         assert build.exists
         assert build.active
         self.assertEqual('Test', build.label)
@@ -142,7 +142,7 @@
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build', redirected_to[0])
 
-        self.assertRaises(Exception, BuildConfig, self.env, 'test')
+        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
 
     def test_edit_config(self):
         config = BuildConfig(self.env)
@@ -183,9 +183,9 @@
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/foo', redirected_to[0])
 
-        self.assertRaises(Exception, BuildConfig, self.env, 'test')
+        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
 
-        build = BuildConfig(self.env, 'foo')
+        build = BuildConfig.fetch(self.env, 'foo')
         assert build.exists
         assert build.active
         self.assertEqual('Test', build.label)
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -43,7 +43,9 @@
         path = os.path.join(sys.prefix, 'share', 'bitten', name)
     return path
 
+
 class BuildModule(Component):
+    """Implements the Bitten web interface."""
 
     implements(INavigationContributor, IRequestHandler, ITimelineEventProvider,
                ITemplateProvider)
@@ -99,8 +101,7 @@
                         self._do_delete_platforms(req)
                         self._render_config_form(req, config)
                     elif 'new' in req.args.keys():
-                        platform = TargetPlatform(self.env)
-                        platform.config = config
+                        platform = TargetPlatform(self.env, config=config)
                         self._render_platform_form(req, platform)
                     else:
                         self._do_save_config(req, config)
@@ -114,10 +115,11 @@
                 if action == 'edit':
                     platform_id = req.args.get('platform')
                     if platform_id:
-                        platform = TargetPlatform(self.env, int(platform_id))
+                        platform = TargetPlatform.fetch(self.env,
+                                                        int(platform_id))
                         self._render_platform_form(req, platform)
                     elif 'new' in req.args.keys():
-                        platform = TargetPlatform(self.env)
+                        platform = TargetPlatform(self.env, config=config)
                         self._render_platform_form(req, platform)
                     else:
                         self._render_config_form(req, config)
@@ -179,12 +181,11 @@
         if 'cancel' in req.args.keys():
             req.redirect(self.env.href.build())
 
-        config = BuildConfig(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 = BuildConfig(self.env, name=req.args.get('name'),
+                             path=req.args.get('path', ''),
+                             label=req.args.get('label', ''),
+                             active=req.args.has_key('active'),
+                             description=req.args.get('description'))
         config.insert()
 
         req.redirect(self.env.href.build(config.name))
@@ -196,7 +197,8 @@
         if 'cancel' in req.args.keys():
             req.redirect(self.env.href.build(config_name))
 
-        config = BuildConfig(self.env, config_name)
+        config = BuildConfig.fetch(self.env, config_name)
+        assert config, 'Build configuration "%s" does not exist' % config_name
         config.name = req.args.get('name')
         config.active = req.args.has_key('active')
         config.label = req.args.get('label', '')
@@ -213,9 +215,8 @@
         if 'cancel' in req.args.keys():
             req.redirect(self.env.href.build(config_name, action='edit'))
 
-        platform = TargetPlatform(self.env)
-        platform.config = config_name
-        platform.name = req.args.get('name')
+        platform = TargetPlatform(self.env, config=config_name,
+                                  name=req.args.get('name'))
 
         properties = [int(key[9:]) for key in req.args.keys()
                       if key.startswith('property_')]
@@ -251,7 +252,7 @@
 
         db = self.env.get_db_cnx()
         for platform_id in [int(id) for id in req.args.get('delete_platform')]:
-            platform = TargetPlatform(self.env, platform_id, db=db)
+            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)
@@ -264,7 +265,7 @@
         if 'cancel' in req.args.keys():
             req.redirect(self.env.href.build(config_name, action='edit'))
 
-        platform = TargetPlatform(self.env, platform_id)
+        platform = TargetPlatform.fetch(self.env, platform_id)
         platform.name = req.args.get('name')
 
         properties = [int(key[9:]) for key in req.args.keys()
@@ -310,7 +311,7 @@
         req.hdf['build.can_create'] = req.perm.has_permission('BUILD_CREATE')
 
     def _render_config(self, req, config_name):
-        config = BuildConfig(self.env, 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')
@@ -344,8 +345,8 @@
                 break
 
     def _render_config_form(self, req, config_name=None):
-        config = BuildConfig(self.env, config_name)
-        if config.exists:
+        config = BuildConfig.fetch(self.env, config_name)
+        if config:
             req.perm.assert_permission('BUILD_MODIFY')
             req.hdf['build.config'] = {
                 'name': config.name, 'label': config.label, 'path': config.path,
@@ -382,8 +383,8 @@
         req.hdf['build.mode'] = 'edit_platform'
 
     def _render_build(self, req, build_id):
-        build = Build(self.env, build_id)
-        assert build.exists
+        build = Build.fetch(self.env, build_id)
+        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',
@@ -393,7 +394,7 @@
         req.hdf['build'] = self._build_to_hdf(build)
         req.hdf['build.mode'] = 'view_build'
 
-        config = BuildConfig(self.env, build.config)
+        config = BuildConfig.fetch(self.env, build.config)
         req.hdf['build.config'] = {
             'name': config.label,
             'href': self.env.href.build(config.name)
Copyright (C) 2012-2017 Edgewall Software