changeset 410:7930cdd83d13

More restructuring: got rid of the `trac_ext` subpackage, which makes no sense now that the master is also coupled to Trac.
author cmlenz
date Tue, 07 Aug 2007 09:49:08 +0000
parents 5786700df0c7
children a169d2e96463
files bitten/api.py bitten/htdocs/bitten.css bitten/htdocs/bitten_build.png bitten/htdocs/bitten_buildf.png bitten/htdocs/charts.swf bitten/htdocs/charts_library/arno.swf bitten/htdocs/charts_library/arst.swf bitten/htdocs/charts_library/brfl.swf bitten/htdocs/charts_library/brno.swf bitten/htdocs/charts_library/brst.swf bitten/htdocs/charts_library/cl3d.swf bitten/htdocs/charts_library/clfl.swf bitten/htdocs/charts_library/clno.swf bitten/htdocs/charts_library/clp3.swf bitten/htdocs/charts_library/cls3.swf bitten/htdocs/charts_library/clst.swf bitten/htdocs/charts_library/cnno.swf bitten/htdocs/charts_library/lnno.swf bitten/htdocs/charts_library/mxno.swf bitten/htdocs/charts_library/pi3d.swf bitten/htdocs/charts_library/pino.swf bitten/htdocs/charts_library/scno.swf bitten/htdocs/failure.png bitten/htdocs/tabset.js bitten/main.py bitten/master.py bitten/report/coverage.py bitten/report/testing.py bitten/report/tests/coverage.py bitten/report/tests/testing.py bitten/templates/bitten_build.cs bitten/templates/bitten_chart_coverage.cs bitten/templates/bitten_chart_tests.cs bitten/templates/bitten_config.cs bitten/templates/bitten_summary_coverage.cs bitten/templates/bitten_summary_tests.cs bitten/tests/__init__.py bitten/tests/master.py bitten/tests/model.py bitten/tests/queue.py bitten/tests/web_ui.py bitten/trac_ext/__init__.py bitten/trac_ext/api.py bitten/trac_ext/compat.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/htdocs/bitten_build.png bitten/trac_ext/htdocs/bitten_buildf.png bitten/trac_ext/htdocs/charts.swf bitten/trac_ext/htdocs/charts_library/arno.swf bitten/trac_ext/htdocs/charts_library/arst.swf bitten/trac_ext/htdocs/charts_library/brfl.swf bitten/trac_ext/htdocs/charts_library/brno.swf bitten/trac_ext/htdocs/charts_library/brst.swf bitten/trac_ext/htdocs/charts_library/cl3d.swf bitten/trac_ext/htdocs/charts_library/clfl.swf bitten/trac_ext/htdocs/charts_library/clno.swf bitten/trac_ext/htdocs/charts_library/clp3.swf bitten/trac_ext/htdocs/charts_library/cls3.swf bitten/trac_ext/htdocs/charts_library/clst.swf bitten/trac_ext/htdocs/charts_library/cnno.swf bitten/trac_ext/htdocs/charts_library/lnno.swf bitten/trac_ext/htdocs/charts_library/mxno.swf bitten/trac_ext/htdocs/charts_library/pi3d.swf bitten/trac_ext/htdocs/charts_library/pino.swf bitten/trac_ext/htdocs/charts_library/scno.swf bitten/trac_ext/htdocs/failure.png bitten/trac_ext/htdocs/tabset.js bitten/trac_ext/main.py bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/templates/bitten_chart_coverage.cs bitten/trac_ext/templates/bitten_chart_tests.cs bitten/trac_ext/templates/bitten_config.cs bitten/trac_ext/templates/bitten_summary_coverage.cs bitten/trac_ext/templates/bitten_summary_tests.cs bitten/trac_ext/tests/__init__.py bitten/trac_ext/tests/web_ui.py bitten/trac_ext/web_ui.py bitten/upgrades.py bitten/web_ui.py setup.py
diffstat 50 files changed, 827 insertions(+), 851 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bitten/api.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.edgewall.org/wiki/License.
+
+"""Interfaces of extension points provided by the Bitten Trac plugin."""
+
+from trac.core import *
+
+
+class IBuildListener(Interface):
+    """Extension point interface for components that need to be notified of
+    build events.
+    
+    Note that these will be notified in the process running the build master,
+    not the web interface.
+    """
+
+    def build_started(build):
+        """Called when a build slave has accepted a build initiation.
+        
+        @param build: the build that was started
+        @type build: an instance of L{bitten.model.Build}
+        """
+
+    def build_aborted(build):
+        """Called when a build slave cancels a build or disconnects.
+        
+        @param build: the build that was aborted
+        @type build: an instance of L{bitten.model.Build}
+        """
+
+    def build_completed(build):
+        """Called when a build slave has completed a build, regardless of the
+        outcome.
+        
+        @param build: the build that was aborted
+        @type build: an instance of L{bitten.model.Build}
+        """
+
+
+class ILogFormatter(Interface):
+    """Extension point interface for components that format build log
+    messages."""
+
+    def get_formatter(req, build):
+        """Return a function that gets called for every log message.
+        
+        The function must take four positional arguments, C{step},
+        C{generator}, C{level} and C{message}, and return the formatted
+        message as a string.
+
+        @param req: the request object
+        @param build: the build to which the logs belong that should be
+            formatted
+        @type build: an instance of L{bitten.model.Build}
+        """
+
+
+class IReportSummarizer(Interface):
+    """Extension point interface for components that render a summary of reports
+    of some kind."""
+
+    def get_supported_categories():
+        """Return a list of strings identifying the types of reports this 
+        component supports."""
+
+    def render_summary(req, config, build, step, category):
+        """Render a summary for the given report and return the resulting HTML
+        as string.
+        
+        @param req: the request object
+        @param config: the build configuration
+        @type config: an instance of L{bitten.model.BuildConfig}
+        @param build: the build
+        @type build: an instance of L{bitten.model.Build}
+        @param step: the build step
+        @type step: an instance of L{bitten.model.BuildStep}
+        @param category: the category of the report that should be summarized
+        """
+
+
+class IReportChartGenerator(Interface):
+    """Extension point interface for components that generator a chart for a
+    set of reports."""
+
+    def get_supported_categories():
+        """Return a list of strings identifying the types of reports this 
+        component supports."""
+
+    def generate_chart_data(req, config, category):
+        """Generate the data for a report chart.
+        
+        This method should store the data in the HDF of the request and return
+        the name of the template that should process the data.
+        
+        @param req: the request object
+        @param config: the build configuration
+        @type config: an instance of L{bitten.model.BuildConfig}
+        @param category: the category of reports to include in the chart
+        """
rename from bitten/trac_ext/htdocs/bitten.css
rename to bitten/htdocs/bitten.css
rename from bitten/trac_ext/htdocs/bitten_build.png
rename to bitten/htdocs/bitten_build.png
rename from bitten/trac_ext/htdocs/bitten_buildf.png
rename to bitten/htdocs/bitten_buildf.png
rename from bitten/trac_ext/htdocs/charts.swf
rename to bitten/htdocs/charts.swf
rename from bitten/trac_ext/htdocs/charts_library/arno.swf
rename to bitten/htdocs/charts_library/arno.swf
rename from bitten/trac_ext/htdocs/charts_library/arst.swf
rename to bitten/htdocs/charts_library/arst.swf
rename from bitten/trac_ext/htdocs/charts_library/brfl.swf
rename to bitten/htdocs/charts_library/brfl.swf
rename from bitten/trac_ext/htdocs/charts_library/brno.swf
rename to bitten/htdocs/charts_library/brno.swf
rename from bitten/trac_ext/htdocs/charts_library/brst.swf
rename to bitten/htdocs/charts_library/brst.swf
rename from bitten/trac_ext/htdocs/charts_library/cl3d.swf
rename to bitten/htdocs/charts_library/cl3d.swf
rename from bitten/trac_ext/htdocs/charts_library/clfl.swf
rename to bitten/htdocs/charts_library/clfl.swf
rename from bitten/trac_ext/htdocs/charts_library/clno.swf
rename to bitten/htdocs/charts_library/clno.swf
rename from bitten/trac_ext/htdocs/charts_library/clp3.swf
rename to bitten/htdocs/charts_library/clp3.swf
rename from bitten/trac_ext/htdocs/charts_library/cls3.swf
rename to bitten/htdocs/charts_library/cls3.swf
rename from bitten/trac_ext/htdocs/charts_library/clst.swf
rename to bitten/htdocs/charts_library/clst.swf
rename from bitten/trac_ext/htdocs/charts_library/cnno.swf
rename to bitten/htdocs/charts_library/cnno.swf
rename from bitten/trac_ext/htdocs/charts_library/lnno.swf
rename to bitten/htdocs/charts_library/lnno.swf
rename from bitten/trac_ext/htdocs/charts_library/mxno.swf
rename to bitten/htdocs/charts_library/mxno.swf
rename from bitten/trac_ext/htdocs/charts_library/pi3d.swf
rename to bitten/htdocs/charts_library/pi3d.swf
rename from bitten/trac_ext/htdocs/charts_library/pino.swf
rename to bitten/htdocs/charts_library/pino.swf
rename from bitten/trac_ext/htdocs/charts_library/scno.swf
rename to bitten/htdocs/charts_library/scno.swf
rename from bitten/trac_ext/htdocs/failure.png
rename to bitten/htdocs/failure.png
rename from bitten/trac_ext/htdocs/tabset.js
rename to bitten/htdocs/tabset.js
new file mode 100644
--- /dev/null
+++ b/bitten/main.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.edgewall.org/wiki/License.
+
+import inspect
+import os
+import textwrap
+
+from trac.core import *
+from trac.db import DatabaseManager
+from trac.env import IEnvironmentSetupParticipant
+from trac.perm import IPermissionRequestor
+from trac.wiki import IWikiSyntaxProvider
+from bitten.api import IBuildListener
+from bitten.model import schema, schema_version, Build, BuildConfig
+
+
+class BuildSystem(Component):
+
+    implements(IEnvironmentSetupParticipant, IPermissionRequestor,
+               IWikiSyntaxProvider)
+
+    listeners = ExtensionPoint(IBuildListener)
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        # Create the required tables
+        db = self.env.get_db_cnx()
+        connector, _ = DatabaseManager(self.env)._get_connector()
+        cursor = db.cursor()
+        for table in schema:
+            for stmt in connector.to_sql(table):
+                cursor.execute(stmt)
+
+        # Insert a global version flag
+        cursor.execute("INSERT INTO system (name,value) "
+                       "VALUES ('bitten_version',%s)", (schema_version,))
+
+        # Create the directory for storing snapshot archives
+        snapshots_dir = os.path.join(self.env.path, 'snapshots')
+        os.mkdir(snapshots_dir)
+
+        db.commit()
+
+    def environment_needs_upgrade(self, db):
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM system WHERE name='bitten_version'")
+        row = cursor.fetchone()
+        if not row or int(row[0]) < schema_version:
+            return True
+
+    def upgrade_environment(self, db):
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM system WHERE name='bitten_version'")
+        row = cursor.fetchone()
+        if not row:
+            self.environment_created()
+        else:
+            current_version = int(row[0])
+            from bitten import upgrades
+            for version in range(current_version + 1, schema_version + 1):
+                for function in upgrades.map.get(version):
+                    print textwrap.fill(inspect.getdoc(function))
+                    function(self.env, db)
+                    print 'Done.'
+            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)
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        actions = ['BUILD_VIEW', 'BUILD_CREATE', 'BUILD_MODIFY', 'BUILD_DELETE',
+                   'BUILD_EXEC']
+        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/master.py
+++ b/bitten/master.py
@@ -30,9 +30,9 @@
                      RequestDone
 
 from bitten.model import BuildConfig, Build, BuildStep, BuildLog, Report
+from bitten.main import BuildSystem
 from bitten.queue import BuildQueue
 from bitten.recipe import Recipe
-from bitten.trac_ext.main import BuildSystem
 from bitten.util import xmlio
 
 
--- a/bitten/report/coverage.py
+++ b/bitten/report/coverage.py
@@ -11,7 +11,7 @@
 from trac.core import *
 from trac.web.chrome import Chrome
 from trac.web.clearsilver import HDFWrapper
-from bitten.trac_ext.api import IReportChartGenerator, IReportSummarizer
+from bitten.api import IReportChartGenerator, IReportSummarizer
 
 
 class TestCoverageChartGenerator(Component):
--- a/bitten/report/testing.py
+++ b/bitten/report/testing.py
@@ -11,7 +11,7 @@
 from trac.core import *
 from trac.web.chrome import Chrome
 from trac.web.clearsilver import HDFWrapper
-from bitten.trac_ext.api import IReportChartGenerator, IReportSummarizer
+from bitten.api import IReportChartGenerator, IReportSummarizer
 
 
 class TestResultsChartGenerator(Component):
--- a/bitten/report/tests/coverage.py
+++ b/bitten/report/tests/coverage.py
@@ -10,11 +10,11 @@
 
 import unittest
 
+from trac.db import DatabaseManager
 from trac.test import EnvironmentStub, Mock
 from trac.web.clearsilver import HDFWrapper
 from bitten.model import *
 from bitten.report.coverage import TestCoverageChartGenerator
-from bitten.trac_ext.compat import schema_to_sql
 
 
 class TestCoverageChartGeneratorTestCase(unittest.TestCase):
@@ -24,8 +24,10 @@
         self.env.path = ''
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
 
     def test_supported_categories(self):
--- a/bitten/report/tests/testing.py
+++ b/bitten/report/tests/testing.py
@@ -10,11 +10,11 @@
 
 import unittest
 
+from trac.db import DatabaseManager
 from trac.test import EnvironmentStub, Mock
 from trac.web.clearsilver import HDFWrapper
 from bitten.model import *
 from bitten.report.testing import TestResultsChartGenerator
-from bitten.trac_ext.compat import schema_to_sql
 
 
 class TestResultsChartGeneratorTestCase(unittest.TestCase):
@@ -22,10 +22,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
 
     def test_supported_categories(self):
rename from bitten/trac_ext/templates/bitten_build.cs
rename to bitten/templates/bitten_build.cs
rename from bitten/trac_ext/templates/bitten_chart_coverage.cs
rename to bitten/templates/bitten_chart_coverage.cs
rename from bitten/trac_ext/templates/bitten_chart_tests.cs
rename to bitten/templates/bitten_chart_tests.cs
rename from bitten/trac_ext/templates/bitten_config.cs
rename to bitten/templates/bitten_config.cs
rename from bitten/trac_ext/templates/bitten_summary_coverage.cs
rename to bitten/templates/bitten_summary_coverage.cs
rename from bitten/trac_ext/templates/bitten_summary_tests.cs
rename to bitten/templates/bitten_summary_tests.cs
--- a/bitten/tests/__init__.py
+++ b/bitten/tests/__init__.py
@@ -10,10 +10,9 @@
 
 import unittest
 
-from bitten.tests import master, model, recipe, queue, slave
+from bitten.tests import master, model, recipe, queue, slave, web_ui
 from bitten.build import tests as build
 from bitten.report import tests as report
-from bitten.trac_ext import tests as trac_ext
 from bitten.util import tests as util
 
 def suite():
@@ -23,9 +22,9 @@
     suite.addTest(recipe.suite())
     suite.addTest(queue.suite())
     suite.addTest(slave.suite())
+    suite.addTest(web_ui.suite())
     suite.addTest(build.suite())
     suite.addTest(report.suite())
-    suite.addTest(trac_ext.suite())
     suite.addTest(util.suite())
     return suite
 
--- a/bitten/tests/master.py
+++ b/bitten/tests/master.py
@@ -15,17 +15,17 @@
 import tempfile
 import unittest
 
+from trac.db import DatabaseManager
 from trac.perm import PermissionCache, PermissionSystem
 from trac.test import EnvironmentStub, Mock
 from trac.web.api import HTTPBadRequest, HTTPMethodNotAllowed, HTTPNotFound, \
                          RequestDone
 from trac.web.href import Href
 
+from bitten.main import BuildSystem
 from bitten.master import BuildMaster
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \
                          BuildLog, Report, schema
-from bitten.trac_ext.compat import schema_to_sql
-from bitten.trac_ext.main import BuildSystem
 
 
 class BuildMasterTestCase(unittest.TestCase):
@@ -39,8 +39,9 @@
         # Create tables
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
 
         self.repos = Mock()
--- a/bitten/tests/model.py
+++ b/bitten/tests/model.py
@@ -10,10 +10,10 @@
 
 import unittest
 
+from trac.db import DatabaseManager
 from trac.test import EnvironmentStub
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \
                          BuildLog, Report, schema
-from bitten.trac_ext.compat import schema_to_sql
 
 
 class BuildConfigTestCase(unittest.TestCase):
@@ -21,10 +21,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
@@ -157,10 +159,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in TargetPlatform._schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
@@ -212,10 +216,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in Build._schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
@@ -319,10 +325,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in BuildStep._schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
@@ -419,10 +427,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in BuildLog._schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
@@ -549,10 +559,12 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = ''
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in Report._schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
--- a/bitten/tests/queue.py
+++ b/bitten/tests/queue.py
@@ -13,10 +13,10 @@
 import tempfile
 import unittest
 
+from trac.db import DatabaseManager
 from trac.test import EnvironmentStub, Mock
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, schema
 from bitten.queue import BuildQueue, collect_changes
-from bitten.trac_ext.compat import schema_to_sql
 
 
 class CollectChangesTestCase(unittest.TestCase):
@@ -27,11 +27,14 @@
     def setUp(self):
         self.env = EnvironmentStub()
         self.env.path = tempfile.mkdtemp()
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
+
         self.config = BuildConfig(self.env, name='test', path='somepath')
         self.config.insert(db=db)
         self.platform = TargetPlatform(self.env, config='test', name='Foo')
@@ -123,10 +126,12 @@
         self.env = EnvironmentStub()
         self.env.path = tempfile.mkdtemp()
         os.mkdir(os.path.join(self.env.path, 'snapshots'))
+
         db = self.env.get_db_cnx()
         cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
         for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
+            for stmt in connector.to_sql(table):
                 cursor.execute(stmt)
         db.commit()
 
new file mode 100644
--- /dev/null
+++ b/bitten/tests/web_ui.py
@@ -0,0 +1,553 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.edgewall.org/wiki/License.
+
+import shutil
+import tempfile
+import unittest
+
+from trac.core import TracError
+from trac.db import DatabaseManager
+from trac.perm import PermissionCache, PermissionSystem
+from trac.test import EnvironmentStub, Mock
+from trac.versioncontrol import Repository
+from trac.web.clearsilver import HDFWrapper
+from trac.web.href import Href
+from trac.web.main import Request, RequestDone
+from bitten.main import BuildSystem
+from bitten.model import BuildConfig, TargetPlatform, Build, schema
+from bitten.web_ui import BuildConfigController
+
+
+class BuildConfigControllerTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = tempfile.mkdtemp()
+
+        # Create tables
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        connector, _ = DatabaseManager(self.env)._get_connector()
+        for table in schema:
+            for stmt in connector.to_sql(table):
+                cursor.execute(stmt)
+
+        # Set up permissions
+        self.env.config.set('trac', 'permission_store',
+                            'DefaultPermissionStore')
+
+        # Hook up a dummy repository
+        self.repos = Mock(
+            get_node=lambda path, rev=None: Mock(get_history=lambda: [],
+                                                 isdir=True),
+            normalize_path=lambda path: path,
+            sync=lambda: None
+        )
+        self.env.get_repository = lambda authname=None: self.repos
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+
+    def test_overview(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), args={}, chrome={},
+                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('overview', req.hdf['page.mode'])
+        self.assertEqual('0', req.hdf.get('build.can_create', '0'))
+
+    def test_overview_admin(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), args={}, chrome={},
+                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('1', req.hdf.get('config.can_create'))
+
+    def test_view_config(self):
+        config = BuildConfig(self.env, name='test', path='trunk')
+        config.insert()
+        platform = TargetPlatform(self.env, config='test', name='any')
+        platform.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'), args={},
+                   chrome={}, hdf=HDFWrapper(), authname='joe',
+                   perm=PermissionCache(self.env, 'joe'))
+
+        root = Mock(get_entries=lambda: ['foo'],
+                    get_history=lambda: [('trunk', rev, 'edit') for rev in
+                                          range(123, 111, -1)])
+        self.repos = Mock(get_node=lambda path, rev=None: root,
+                          sync=lambda: None, normalize_path=lambda path: path)
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('view_config', req.hdf['page.mode'])
+        self.assertEqual('0', req.hdf.get('build.config.can_delete', '0'))
+        self.assertEqual('0', req.hdf.get('build.config.can_modify', '0'))
+        self.assertEqual(None, req.hdf.get('chrome.links.next.0.href'))
+
+    def test_view_config_paging(self):
+        config = BuildConfig(self.env, name='test', path='trunk')
+        config.insert()
+        platform = TargetPlatform(self.env, config='test', name='any')
+        platform.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'), args={},
+                   chrome={}, hdf=HDFWrapper(), authname='joe',
+                   perm=PermissionCache(self.env, 'joe'))
+
+        root = Mock(get_entries=lambda: ['foo'],
+                    get_history=lambda: [('trunk', rev, 'edit') for rev in
+                                          range(123, 110, -1)])
+        self.repos = Mock(get_node=lambda path, rev=None: root,
+                          sync=lambda: None, normalize_path=lambda path: path)
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        if req.chrome: # Trac 0.11
+            self.assertEqual('/trac/build/test?page=2',
+                             req.chrome['links']['next'][0]['href'])
+        else:
+            self.assertEqual('/trac/build/test?page=2',
+                             req.hdf.get('chrome.links.next.0.href'))
+
+    def test_view_config_admin(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'), args={},
+                   chrome={}, hdf=HDFWrapper(), authname='joe',
+                   perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('1', req.hdf.get('config.can_delete'))
+        self.assertEqual('1', req.hdf.get('config.can_modify'))
+
+    def test_new_config(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build', args={'action': 'new'}, hdf=HDFWrapper(),
+                   href=Href('/trac'), chrome={},
+                   perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('edit_config', req.hdf['page.mode'])
+
+    def test_new_config_submit(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), redirect=redirect,
+                   hdf=HDFWrapper(), authname='joe',
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test', redirected_to[0])
+
+        config = BuildConfig.fetch(self.env, 'test')
+        assert config.exists
+        assert not config.active
+        self.assertEqual('Test', config.label)
+        self.assertEqual('test/trunk', config.path)
+        self.assertEqual('Bla bla', config.description)
+
+    def test_new_config_submit_without_name(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': '', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(TracError, module.process_request, req)
+
+    def test_new_config_submit_with_invalid_name(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'Foo bar',
+                         'path': 'test/trunk', 'label': 'Test',
+                         'description': 'Bla bla'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(TracError, module.process_request, req)
+
+    def test_new_config_submit_invalid_path(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
+                   authname='joe', perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla'})
+
+        def get_node(path, rev=None):
+            raise TracError('No such node')
+        self.repos = Mock(get_node=get_node)
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(TracError, module.process_request, req)
+
+    def test_new_config_submit_with_non_wellformed_recipe(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
+                   authname='joe', perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla',
+                         'recipe': '<build><step>'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(TracError, module.process_request, req)
+
+    def test_new_config_submit_with_invalid_recipe(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
+                   authname='joe', perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
+                         'label': 'Test', 'description': 'Bla bla',
+                         'recipe': '<build><step/></build>'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(TracError, module.process_request, req)
+
+    def test_new_config_cancel(self):
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build', href=Href('/trac'), redirect=redirect,
+                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'cancel': '1', 'name': 'test'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build', redirected_to[0])
+
+        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
+
+    def test_delete_config(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'), chrome={},
+                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'delete'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('delete_config', req.hdf['page.mode'])
+
+    def test_delete_config_submit(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'delete'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build', redirected_to[0])
+
+        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
+
+    def test_edit_config_cancel(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'delete', 'cancel': ''})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test', redirected_to[0])
+
+        self.assertEqual(True, BuildConfig.fetch(self.env, 'test').exists)
+
+    def test_edit_config(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', hdf=HDFWrapper(),
+                   href=Href('/build/test'), chrome={},
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'edit'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('edit_config', req.hdf['page.mode'])
+
+    def test_edit_config_submit(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   authname='joe', perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'edit', 'name': 'foo', 'path': 'test/trunk',
+                         'label': 'Test',  'description': 'Bla bla'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/foo', redirected_to[0])
+
+        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
+
+        config = BuildConfig.fetch(self.env, 'foo')
+        assert config.exists
+        self.assertEqual('Test', config.label)
+        self.assertEqual('test/trunk', config.path)
+        self.assertEqual('Bla bla', config.description)
+
+    def test_edit_config_cancel(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'edit', 'cancel': ''})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test', redirected_to[0])
+
+    def test_new_platform(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', hdf=HDFWrapper(), href=Href('trac'),
+                   chrome={}, perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'edit', 'new': '1'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('edit_platform', req.hdf['page.mode'])
+
+    def test_new_platform_submit(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'name': 'Test'})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
+
+    def test_new_platform_cancel(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+ 
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'new', 'cancel': ''})
+ 
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
+
+    def test_edit_platform(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+        platform = TargetPlatform(self.env)
+        platform.config = 'test'
+        platform.name = 'linux'
+        platform.rules.append(('os', 'linux?'))
+        platform.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        req = Mock(method='GET', base_path='', cgi_location='',
+                   path_info='/build/test', hdf=HDFWrapper(),
+                   href=Href('/trac'), chrome={},
+                   perm=PermissionCache(self.env, 'joe'),
+                   args={'action': 'edit', 'platform': platform.id})
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        module.process_request(req)
+
+        self.assertEqual('edit_platform', req.hdf['page.mode'])
+
+    def test_edit_platform_submit(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+        platform = TargetPlatform(self.env)
+        platform.config = 'test'
+        platform.name = 'linux'
+        platform.rules.append(('os', 'linux?'))
+        platform.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   args={'action': 'edit', 'platform': platform.id,
+                         'name': 'Test'},
+                   perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
+
+    def test_edit_platform_cancel(self):
+        config = BuildConfig(self.env)
+        config.name = 'test'
+        config.insert()
+        platform = TargetPlatform(self.env)
+        platform.config = 'test'
+        platform.name = 'linux'
+        platform.rules.append(('os', 'linux'))
+        platform.insert()
+
+        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
+        redirected_to = []
+        def redirect(url):
+            redirected_to.append(url)
+            raise RequestDone
+        req = Mock(method='POST', base_path='', cgi_location='',
+                   path_info='/build/test', href=Href('/trac'),
+                   redirect=redirect, hdf=HDFWrapper(),
+                   args={'action': 'edit', 'platform': platform.id,
+                         'cancel': ''},
+                   perm=PermissionCache(self.env, 'joe'))
+
+        module = BuildConfigController(self.env)
+        assert module.match_request(req)
+        self.assertRaises(RequestDone, module.process_request, req)
+        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
+
+
+def suite():
+    return unittest.makeSuite(BuildConfigControllerTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
deleted file mode 100644
--- a/bitten/trac_ext/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-"""Implementation of the Bitten plugin for Trac."""
-
-from bitten.trac_ext.api import *
deleted file mode 100644
--- a/bitten/trac_ext/api.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-"""Interfaces of extension points provided by the Bitten Trac plugin."""
-
-from trac.core import *
-
-
-class IBuildListener(Interface):
-    """Extension point interface for components that need to be notified of
-    build events.
-    
-    Note that these will be notified in the process running the build master,
-    not the web interface.
-    """
-
-    def build_started(build):
-        """Called when a build slave has accepted a build initiation.
-        
-        @param build: the build that was started
-        @type build: an instance of L{bitten.model.Build}
-        """
-
-    def build_aborted(build):
-        """Called when a build slave cancels a build or disconnects.
-        
-        @param build: the build that was aborted
-        @type build: an instance of L{bitten.model.Build}
-        """
-
-    def build_completed(build):
-        """Called when a build slave has completed a build, regardless of the
-        outcome.
-        
-        @param build: the build that was aborted
-        @type build: an instance of L{bitten.model.Build}
-        """
-
-
-class ILogFormatter(Interface):
-    """Extension point interface for components that format build log
-    messages."""
-
-    def get_formatter(req, build):
-        """Return a function that gets called for every log message.
-        
-        The function must take four positional arguments, C{step},
-        C{generator}, C{level} and C{message}, and return the formatted
-        message as a string.
-
-        @param req: the request object
-        @param build: the build to which the logs belong that should be
-            formatted
-        @type build: an instance of L{bitten.model.Build}
-        """
-
-
-class IReportSummarizer(Interface):
-    """Extension point interface for components that render a summary of reports
-    of some kind."""
-
-    def get_supported_categories():
-        """Return a list of strings identifying the types of reports this 
-        component supports."""
-
-    def render_summary(req, config, build, step, category):
-        """Render a summary for the given report and return the resulting HTML
-        as string.
-        
-        @param req: the request object
-        @param config: the build configuration
-        @type config: an instance of L{bitten.model.BuildConfig}
-        @param build: the build
-        @type build: an instance of L{bitten.model.Build}
-        @param step: the build step
-        @type step: an instance of L{bitten.model.BuildStep}
-        @param category: the category of the report that should be summarized
-        """
-
-
-class IReportChartGenerator(Interface):
-    """Extension point interface for components that generator a chart for a
-    set of reports."""
-
-    def get_supported_categories():
-        """Return a list of strings identifying the types of reports this 
-        component supports."""
-
-    def generate_chart_data(req, config, category):
-        """Generate the data for a report chart.
-        
-        This method should store the data in the HDF of the request and return
-        the name of the template that should process the data.
-        
-        @param req: the request object
-        @param config: the build configuration
-        @type config: an instance of L{bitten.model.BuildConfig}
-        @param category: the category of reports to include in the chart
-        """
deleted file mode 100644
--- a/bitten/trac_ext/compat.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2006-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-"""Various methods for backwards compatibility with older Trac versions."""
-
-def schema_to_sql(env, db, table):
-    try:
-        # Trac >= 0.10
-        from trac.db import DatabaseManager
-        connector, _ = DatabaseManager(env)._get_connector()
-        return connector.to_sql(table)
-    except ImportError:
-        # Trac 0.9.x
-        return db.to_sql(table)
deleted file mode 100644
--- a/bitten/trac_ext/main.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-import inspect
-import os
-import textwrap
-
-from trac.core import *
-from trac.env import IEnvironmentSetupParticipant
-from trac.perm import IPermissionRequestor
-from trac.wiki import IWikiSyntaxProvider
-from bitten.trac_ext.api import IBuildListener
-from bitten.trac_ext.compat import schema_to_sql
-from bitten.model import schema, schema_version, Build, BuildConfig
-
-
-class BuildSystem(Component):
-
-    implements(IEnvironmentSetupParticipant, IPermissionRequestor,
-               IWikiSyntaxProvider)
-
-    listeners = ExtensionPoint(IBuildListener)
-
-    # IEnvironmentSetupParticipant methods
-
-    def environment_created(self):
-        # Create the required tables
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
-                cursor.execute(stmt)
-
-        # Insert a global version flag
-        cursor.execute("INSERT INTO system (name,value) "
-                       "VALUES ('bitten_version',%s)", (schema_version,))
-
-        # Create the directory for storing snapshot archives
-        snapshots_dir = os.path.join(self.env.path, 'snapshots')
-        os.mkdir(snapshots_dir)
-
-        db.commit()
-
-    def environment_needs_upgrade(self, db):
-        cursor = db.cursor()
-        cursor.execute("SELECT value FROM system WHERE name='bitten_version'")
-        row = cursor.fetchone()
-        if not row or int(row[0]) < schema_version:
-            return True
-
-    def upgrade_environment(self, db):
-        cursor = db.cursor()
-        cursor.execute("SELECT value FROM system WHERE name='bitten_version'")
-        row = cursor.fetchone()
-        if not row:
-            self.environment_created()
-        else:
-            current_version = int(row[0])
-            from bitten import upgrades
-            for version in range(current_version + 1, schema_version + 1):
-                for function in upgrades.map.get(version):
-                    print textwrap.fill(inspect.getdoc(function))
-                    function(self.env, db)
-                    print 'Done.'
-            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)
-
-    # IPermissionRequestor methods
-
-    def get_permission_actions(self):
-        actions = ['BUILD_VIEW', 'BUILD_CREATE', 'BUILD_MODIFY', 'BUILD_DELETE',
-                   'BUILD_EXEC']
-        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
deleted file mode 100644
--- a/bitten/trac_ext/tests/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-import unittest
-
-from bitten.trac_ext.tests import web_ui
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(web_ui.suite())
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
deleted file mode 100644
--- a/bitten/trac_ext/tests/web_ui.py
+++ /dev/null
@@ -1,552 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://bitten.edgewall.org/wiki/License.
-
-import shutil
-import tempfile
-import unittest
-
-from trac.core import TracError
-from trac.perm import PermissionCache, PermissionSystem
-from trac.test import EnvironmentStub, Mock
-from trac.versioncontrol import Repository
-from trac.web.clearsilver import HDFWrapper
-from trac.web.href import Href
-from trac.web.main import Request, RequestDone
-from bitten.model import BuildConfig, TargetPlatform, Build, schema
-from bitten.trac_ext.compat import schema_to_sql
-from bitten.trac_ext.main import BuildSystem
-from bitten.trac_ext.web_ui import BuildConfigController
-
-
-class BuildConfigControllerTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.env = EnvironmentStub()
-        self.env.path = tempfile.mkdtemp()
-
-        # Create tables
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
-                cursor.execute(stmt)
-
-        # Set up permissions
-        self.env.config.set('trac', 'permission_store',
-                            'DefaultPermissionStore')
-
-        # Hook up a dummy repository
-        self.repos = Mock(
-            get_node=lambda path, rev=None: Mock(get_history=lambda: [],
-                                                 isdir=True),
-            normalize_path=lambda path: path,
-            sync=lambda: None
-        )
-        self.env.get_repository = lambda authname=None: self.repos
-
-    def tearDown(self):
-        shutil.rmtree(self.env.path)
-
-    def test_overview(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), args={}, chrome={},
-                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('overview', req.hdf['page.mode'])
-        self.assertEqual('0', req.hdf.get('build.can_create', '0'))
-
-    def test_overview_admin(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), args={}, chrome={},
-                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('1', req.hdf.get('config.can_create'))
-
-    def test_view_config(self):
-        config = BuildConfig(self.env, name='test', path='trunk')
-        config.insert()
-        platform = TargetPlatform(self.env, config='test', name='any')
-        platform.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'), args={},
-                   chrome={}, hdf=HDFWrapper(), authname='joe',
-                   perm=PermissionCache(self.env, 'joe'))
-
-        root = Mock(get_entries=lambda: ['foo'],
-                    get_history=lambda: [('trunk', rev, 'edit') for rev in
-                                          range(123, 111, -1)])
-        self.repos = Mock(get_node=lambda path, rev=None: root,
-                          sync=lambda: None, normalize_path=lambda path: path)
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('view_config', req.hdf['page.mode'])
-        self.assertEqual('0', req.hdf.get('build.config.can_delete', '0'))
-        self.assertEqual('0', req.hdf.get('build.config.can_modify', '0'))
-        self.assertEqual(None, req.hdf.get('chrome.links.next.0.href'))
-
-    def test_view_config_paging(self):
-        config = BuildConfig(self.env, name='test', path='trunk')
-        config.insert()
-        platform = TargetPlatform(self.env, config='test', name='any')
-        platform.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'), args={},
-                   chrome={}, hdf=HDFWrapper(), authname='joe',
-                   perm=PermissionCache(self.env, 'joe'))
-
-        root = Mock(get_entries=lambda: ['foo'],
-                    get_history=lambda: [('trunk', rev, 'edit') for rev in
-                                          range(123, 110, -1)])
-        self.repos = Mock(get_node=lambda path, rev=None: root,
-                          sync=lambda: None, normalize_path=lambda path: path)
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        if req.chrome: # Trac 0.11
-            self.assertEqual('/trac/build/test?page=2',
-                             req.chrome['links']['next'][0]['href'])
-        else:
-            self.assertEqual('/trac/build/test?page=2',
-                             req.hdf.get('chrome.links.next.0.href'))
-
-    def test_view_config_admin(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'), args={},
-                   chrome={}, hdf=HDFWrapper(), authname='joe',
-                   perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('1', req.hdf.get('config.can_delete'))
-        self.assertEqual('1', req.hdf.get('config.can_modify'))
-
-    def test_new_config(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build', args={'action': 'new'}, hdf=HDFWrapper(),
-                   href=Href('/trac'), chrome={},
-                   perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('edit_config', req.hdf['page.mode'])
-
-    def test_new_config_submit(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), redirect=redirect,
-                   hdf=HDFWrapper(), authname='joe',
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
-                         'label': 'Test', 'description': 'Bla bla'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test', redirected_to[0])
-
-        config = BuildConfig.fetch(self.env, 'test')
-        assert config.exists
-        assert not config.active
-        self.assertEqual('Test', config.label)
-        self.assertEqual('test/trunk', config.path)
-        self.assertEqual('Bla bla', config.description)
-
-    def test_new_config_submit_without_name(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': '', 'path': 'test/trunk',
-                         'label': 'Test', 'description': 'Bla bla'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(TracError, module.process_request, req)
-
-    def test_new_config_submit_with_invalid_name(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'Foo bar',
-                         'path': 'test/trunk', 'label': 'Test',
-                         'description': 'Bla bla'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(TracError, module.process_request, req)
-
-    def test_new_config_submit_invalid_path(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
-                   authname='joe', perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
-                         'label': 'Test', 'description': 'Bla bla'})
-
-        def get_node(path, rev=None):
-            raise TracError('No such node')
-        self.repos = Mock(get_node=get_node)
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(TracError, module.process_request, req)
-
-    def test_new_config_submit_with_non_wellformed_recipe(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
-                   authname='joe', perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
-                         'label': 'Test', 'description': 'Bla bla',
-                         'recipe': '<build><step>'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(TracError, module.process_request, req)
-
-    def test_new_config_submit_with_invalid_recipe(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), hdf=HDFWrapper(),
-                   authname='joe', perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'test', 'path': 'test/trunk',
-                         'label': 'Test', 'description': 'Bla bla',
-                         'recipe': '<build><step/></build>'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(TracError, module.process_request, req)
-
-    def test_new_config_cancel(self):
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build', href=Href('/trac'), redirect=redirect,
-                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'cancel': '1', 'name': 'test'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build', redirected_to[0])
-
-        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
-
-    def test_delete_config(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'), chrome={},
-                   hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'delete'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('delete_config', req.hdf['page.mode'])
-
-    def test_delete_config_submit(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'delete'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build', redirected_to[0])
-
-        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
-
-    def test_edit_config_cancel(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'delete', 'cancel': ''})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test', redirected_to[0])
-
-        self.assertEqual(True, BuildConfig.fetch(self.env, 'test').exists)
-
-    def test_edit_config(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', hdf=HDFWrapper(),
-                   href=Href('/build/test'), chrome={},
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'edit'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('edit_config', req.hdf['page.mode'])
-
-    def test_edit_config_submit(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   authname='joe', perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'edit', 'name': 'foo', 'path': 'test/trunk',
-                         'label': 'Test',  'description': 'Bla bla'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/foo', redirected_to[0])
-
-        self.assertEqual(None, BuildConfig.fetch(self.env, 'test'))
-
-        config = BuildConfig.fetch(self.env, 'foo')
-        assert config.exists
-        self.assertEqual('Test', config.label)
-        self.assertEqual('test/trunk', config.path)
-        self.assertEqual('Bla bla', config.description)
-
-    def test_edit_config_cancel(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'edit', 'cancel': ''})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test', redirected_to[0])
-
-    def test_new_platform(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', hdf=HDFWrapper(), href=Href('trac'),
-                   chrome={}, perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'edit', 'new': '1'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('edit_platform', req.hdf['page.mode'])
-
-    def test_new_platform_submit(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'name': 'Test'})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
-
-    def test_new_platform_cancel(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
- 
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'new', 'cancel': ''})
- 
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
-
-    def test_edit_platform(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-        platform = TargetPlatform(self.env)
-        platform.config = 'test'
-        platform.name = 'linux'
-        platform.rules.append(('os', 'linux?'))
-        platform.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        req = Mock(method='GET', base_path='', cgi_location='',
-                   path_info='/build/test', hdf=HDFWrapper(),
-                   href=Href('/trac'), chrome={},
-                   perm=PermissionCache(self.env, 'joe'),
-                   args={'action': 'edit', 'platform': platform.id})
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        module.process_request(req)
-
-        self.assertEqual('edit_platform', req.hdf['page.mode'])
-
-    def test_edit_platform_submit(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-        platform = TargetPlatform(self.env)
-        platform.config = 'test'
-        platform.name = 'linux'
-        platform.rules.append(('os', 'linux?'))
-        platform.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   args={'action': 'edit', 'platform': platform.id,
-                         'name': 'Test'},
-                   perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
-
-    def test_edit_platform_cancel(self):
-        config = BuildConfig(self.env)
-        config.name = 'test'
-        config.insert()
-        platform = TargetPlatform(self.env)
-        platform.config = 'test'
-        platform.name = 'linux'
-        platform.rules.append(('os', 'linux'))
-        platform.insert()
-
-        PermissionSystem(self.env).grant_permission('joe', 'BUILD_ADMIN')
-        redirected_to = []
-        def redirect(url):
-            redirected_to.append(url)
-            raise RequestDone
-        req = Mock(method='POST', base_path='', cgi_location='',
-                   path_info='/build/test', href=Href('/trac'),
-                   redirect=redirect, hdf=HDFWrapper(),
-                   args={'action': 'edit', 'platform': platform.id,
-                         'cancel': ''},
-                   perm=PermissionCache(self.env, 'joe'))
-
-        module = BuildConfigController(self.env)
-        assert module.match_request(req)
-        self.assertRaises(RequestDone, module.process_request, req)
-        self.assertEqual('/trac/build/test?action=edit', redirected_to[0])
-
-
-def suite():
-    return unittest.makeSuite(BuildConfigControllerTestCase, 'test')
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
--- a/bitten/upgrades.py
+++ b/bitten/upgrades.py
@@ -14,15 +14,16 @@
 import os
 import sys
 
-from bitten.trac_ext.compat import schema_to_sql
+from trac.db import DatabaseManager
 
 def add_log_table(env, db):
     """Add a table for storing the builds logs."""
     from bitten.model import BuildLog, BuildStep
     cursor = db.cursor()
 
+    connector, _ = DatabaseManager(env)._get_connector()
     for table in BuildLog._schema:
-        for stmt in schema_to_sql(env, db, table):
+        for stmt in connector.to_sql(table):
             cursor.execute(stmt)
 
     cursor.execute("SELECT build,name,log FROM bitten_step "
@@ -35,7 +36,7 @@
     cursor.execute("CREATE TEMP TABLE old_step AS SELECT * FROM bitten_step")
     cursor.execute("DROP TABLE bitten_step")
     for table in BuildStep._schema:
-        for stmt in schema_to_sql(env, db, table):
+        for stmt in connector.to_sql(table):
             cursor.execute(stmt)
     cursor.execute("INSERT INTO bitten_step (build,name,description,status,"
                    "started,stopped) SELECT build,name,description,status,"
@@ -44,16 +45,18 @@
 def add_recipe_to_config(env, db):
     """Add a column for storing the build recipe to the build configuration
     table."""
-
     from bitten.model import BuildConfig
     cursor = db.cursor()
 
     cursor.execute("CREATE TEMP TABLE old_config AS "
                    "SELECT * FROM bitten_config")
     cursor.execute("DROP TABLE bitten_config")
+
+    connector, _ = DatabaseManager(env)._get_connector()
     for table in BuildConfig._schema:
-        for stmt in schema_to_sql(env, db, table):
+        for stmt in connector.to_sql(table):
             cursor.execute(stmt)
+
     cursor.execute("INSERT INTO bitten_config (name,path,active,recipe,min_rev,"
                    "max_rev,label,description) SELECT name,path,0,'',NULL,"
                    "NULL,label,description FROM old_config")
@@ -112,8 +115,11 @@
     cursor.execute("CREATE TEMP TABLE old_log AS "
                    "SELECT * FROM bitten_log")
     cursor.execute("DROP TABLE bitten_log")
-    for stmt in schema_to_sql(env, db, BuildLog._schema[0]):
+
+    connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in connector.to_sql(BuildLog._schema[0]):
         cursor.execute(stmt)
+
     cursor.execute("INSERT INTO bitten_log (id,build,step,generator,orderno) "
                    "SELECT id,build,step,type,0 FROM old_log")
 
@@ -122,8 +128,9 @@
     from bitten.model import Report
     cursor = db.cursor()
 
+    connector, _ = DatabaseManager(env)._get_connector()
     for table in Report._schema:
-        for stmt in schema_to_sql(env, db, table):
+        for stmt in connector.to_sql(table):
             cursor.execute(stmt)
 
 def xmldb_to_db(env, db):
@@ -267,7 +274,9 @@
                 Column('orderno', type='int')
             ]
     cursor = db.cursor()
-    for stmt in schema_to_sql(env, db, table):
+
+    connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in connector.to_sql(table):
         cursor.execute(stmt)
 
 map = {
rename from bitten/trac_ext/web_ui.py
rename to bitten/web_ui.py
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/web_ui.py
@@ -31,12 +31,11 @@
 from trac.web.chrome import INavigationContributor, ITemplateProvider, \
                             add_link, add_stylesheet
 from trac.wiki import wiki_to_html, wiki_to_oneliner
+from bitten.api import ILogFormatter, IReportChartGenerator, IReportSummarizer
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, \
                          BuildLog, Report
 from bitten.queue import collect_changes
 from bitten.recipe import Recipe, InvalidRecipeError
-from bitten.trac_ext.api import ILogFormatter, IReportChartGenerator, \
-                                IReportSummarizer
 from bitten.util import xmlio
 
 _status_label = {Build.PENDING: 'pending',
--- a/setup.py
+++ b/setup.py
@@ -30,9 +30,9 @@
 
     packages=find_packages(exclude=['*.tests*']),
     package_data={
-        'bitten.trac_ext': ['htdocs/*.*',
-                            'htdocs/charts_library/*.swf',
-                            'templates/*.cs']
+        'bitten': ['htdocs/*.*',
+                   'htdocs/charts_library/*.swf',
+                   'templates/*.cs']
     },
     test_suite='bitten.tests.suite',
     entry_points = {
@@ -43,9 +43,9 @@
             'unittest = bitten.util.testrunner:unittest'
         ],
         'trac.plugins': [
-            'bitten.main = bitten.trac_ext.main',
+            'bitten.main = bitten.main',
             'bitten.master = bitten.master',
-            'bitten.web_ui = bitten.trac_ext.web_ui',
+            'bitten.web_ui = bitten.web_ui',
             'bitten.testing = bitten.report.testing',
             'bitten.coverage = bitten.report.coverage'
         ],
Copyright (C) 2012-2017 Edgewall Software