changeset 141:0e21778c04ef

Refactoring: split up the components and templates that render the web interface.
author cmlenz
date Thu, 18 Aug 2005 10:14:21 +0000
parents 75a6af157f05
children 5a27ec93100d
files bitten/trac_ext/api.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/htdocs/bitten_build.png bitten/trac_ext/htdocs/build.css bitten/trac_ext/htdocs/build.png bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/templates/bitten_config.cs bitten/trac_ext/templates/build.cs bitten/trac_ext/tests/web_ui.py bitten/trac_ext/web_ui.py
diffstat 10 files changed, 414 insertions(+), 360 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bitten/trac_ext/api.py
@@ -0,0 +1,33 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+#
+# Bitten is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.core import *
+
+
+class ILogFormatter(Interface):
+    """Extension point interface for components that format build log
+    messages."""
+
+    def get_formatter(req, build, step, type):
+        """Return a function that gets called for every log message.
+        
+        The function must take two positional arguments, `level` and `message`,
+        and return the formatted message.
+        """
new file mode 100644
--- /dev/null
+++ b/bitten/trac_ext/htdocs/bitten.css
@@ -0,0 +1,31 @@
+/* Timeline styles */
+#content.timeline dt.successbuild, #content.timeline dt.successbuild a,
+#content.timeline dt.failedbuild, #content.timeline dt.failedbuild a {
+ background-image: url(bitten_build.png) !important
+}
+
+#content.build form.config td.active { vertical-align: bottom; }
+#content.build form.platforms ul { list-style: none; padding-left: 1em; }
+
+#content.build #builds { margin-top: 2em; }
+#content.build #builds th.rev { width: 6em; }
+#content.build #builds td :link, #content.build #builds td :visited {
+ font-weight: bold;
+}
+#content.build #builds td.completed { background: #9d9; }
+#content.build #builds td.failed { background: #d99; }
+#content.build #builds td.in-progress { background: #ff9; }
+
+#content.build .reports {
+ background: #d7d7d7;
+ float: right;
+ font-size: 90%;
+ margin: .5em 0 .5em 1em;
+ padding: .5em;
+}
+#content.build .reports h3 { margin: 0; }
+#content.build .reports ul { margin: 0; padding: 0; list-style: none; }
+
+#content.build .log { clear: right; overflow: auto; white-space: pre; }
+#content.build .log .warning { color: #660; font-weight: bold; }
+#content.build .log .error { color: #900; font-weight: bold; }
new file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1e2f5669121b121d8119ca1ca05ff66663a41075
GIT binary patch
literal 300
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CH6Vd_r9R
z|NsB&*|Vd^PoG~B(Kl^QOH0e_8mF|hwBjhusHi9_D=SMa-YEH<-$7=T1o;Is{6_%J
zBLQkaan1sd$YKTtZXpn6ymYtj4^U9C#5JNMI6tkVJh3R1!8b9vC_gtfB{NaMEwd=K
zJijQrSiwZk;FX$sDNwN(NU?KKYGO%dex5=|W^O8jfw{hcp}v8s@ER``pb8657srr_
zImrnLAwek&Oo7(}7}?m4P0(;<6-c-A$O#MC$vB}aC1N7aBcp=tk_|6qZ|sy!=t^W}
ZIP#C{{ok5>vOqH!JYD@<);T3K0RZWzW`O_z
deleted file mode 100644
--- a/bitten/trac_ext/htdocs/build.css
+++ /dev/null
@@ -1,31 +0,0 @@
-/* Timeline styles */
-#content.timeline dt.successbuild, #content.timeline dt.successbuild a,
-#content.timeline dt.failedbuild, #content.timeline dt.failedbuild a {
- background-image: url(build.png) !important
-}
-
-#content.build form.config td.active { vertical-align: bottom; }
-#content.build form.platforms ul { list-style: none; padding-left: 1em; }
-
-#content.build #builds { margin-top: 2em; }
-#content.build #builds th.rev { width: 6em; }
-#content.build #builds td :link, #content.build #builds td :visited {
- font-weight: bold;
-}
-#content.build #builds td.completed { background: #9d9; }
-#content.build #builds td.failed { background: #d99; }
-#content.build #builds td.in-progress { background: #ff9; }
-
-#content.build .reports {
- background: #d7d7d7;
- float: right;
- font-size: 90%;
- margin: .5em 0 .5em 1em;
- padding: .5em;
-}
-#content.build .reports h3 { margin: 0; }
-#content.build .reports ul { margin: 0; padding: 0; list-style: none; }
-
-#content.build .log { clear: right; overflow: auto; white-space: pre; }
-#content.build .log .warning { color: #660; font-weight: bold; }
-#content.build .log .error { color: #900; font-weight: bold; }
deleted file mode 100755
index 1e2f5669121b121d8119ca1ca05ff66663a41075..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
--- /dev/null
+++ b/bitten/trac_ext/templates/bitten_build.cs
@@ -0,0 +1,33 @@
+<?cs include:"header.cs" ?>
+ <div id="ctxtnav" class="nav"></div>
+ <div id="content" class="build">
+  <h1><?cs var:title ?></h1>
+  <p class="trigger">Triggered by: Changeset <a href="<?cs
+    var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> of <a href="<?cs
+    var:build.config.href ?>"><?cs var:build.config.name ?></a></p>
+  <p class="slave">Built by: <strong title="<?cs
+    var:build.slave.ip_address ?>"><?cs var:build.slave.name ?></strong> (<?cs
+    var:build.slave.os ?> <?cs var:build.slave.os.version ?><?cs
+    if:build.slave.machine ?> on <?cs var:build.slave.machine ?><?cs
+    /if ?>)</p>
+  <p class="time">Completed: <?cs var:build.started ?> (<?cs
+    var:build.started_delta ?> ago)<br />Took: <?cs var:build.duration ?></p><?cs
+  each:step = build.steps ?>
+   <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
+     var:step.duration ?>)</h2><?cs
+   if:len(step.reports) ?>
+    <div class="reports"><h3>Generated Reports</h3><ul><?cs
+     each:report = step.reports ?><li class="<?cs
+       var:report.type ?>"><a href="<?cs var:report.href ?>"><?cs
+       var:report.type ?></a></li><?cs
+     /each ?>
+    </ul></div><?cs
+   /if ?>
+   <p><?cs var:step.description ?></p>
+   <div class="log"><?cs
+    each:item = step.log ?><code class="<?cs var:item.level ?>"><?cs
+     var:item.message ?></code><br /><?cs
+    /each ?></div><?cs
+  /each ?>
+ </div>
+<?cs include:"footer.cs" ?>
new file mode 100644
--- /dev/null
+++ b/bitten/trac_ext/templates/bitten_config.cs
@@ -0,0 +1,155 @@
+<?cs include:"header.cs" ?>
+ <div id="ctxtnav" class="nav"></div>
+ <div id="content" class="build">
+  <h1><?cs var:title ?></h1><?cs
+
+  if:page.mode == 'overview' ?><?cs
+   each:config = configs ?>
+    <h2><a href="<?cs var:config.href ?>"><?cs var:config.label ?></a></h2><?cs
+    if:config.description ?><div class="description"><?cs
+     var:config.description ?></div><?cs
+    /if ?><?cs
+   /each ?><?cs
+   if:config.can_create ?><div class="buttons">
+    <form method="get" action=""><div>
+     <input type="hidden" name="action" value="new" />
+     <input type="submit" value="Add configuration" />
+    </div></form></div><?cs
+   /if ?></div><?cs
+
+  elif:page.mode == 'edit_config' ?>
+   <form class="config" method="post" action="">
+    <table><tr>
+     <td class="name"><label>Name:<br />
+      <input type="text" name="name" value="<?cs var:config.name ?>" />
+     </label></td>
+     <td class="label"><label>Label (for display):<br />
+      <input type="text" name="label" size="32" value="<?cs
+        var:config.label ?>" />
+     </label></td>
+    </tr><tr>
+     <td class="active"><label><input type="checkbox" name="active"<?cs
+       if:config.active ?> checked="checked"<?cs /if ?> /> Active
+     </label></td>
+     <td class="path"><label>Repository path:<br />
+      <input type="text" name="path" size="48" value="<?cs
+        var:config.path ?>" />
+     </label></td>
+    </tr><tr>
+     <td colspan="2"><fieldset class="iefix">
+      <label for="description">Description (you may use <a tabindex="42" href="<?cs
+        var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label>
+      <p><textarea id="description" name="description" class="wikitext" rows="5" cols="78"><?cs
+        var:config.description ?></textarea></p>
+      <script type="text/javascript" src="<?cs
+        var:htdocs_location ?>js/wikitoolbar.js"></script>
+     </fieldset></td>
+    </tr></table>
+    <div class="buttons">
+     <input type="hidden" name="action" value="<?cs
+       if:config.exists ?>edit<?cs else ?>new<?cs /if ?>" />
+     <input type="submit" value="<?cs
+       if:config.exists ?>Save changes<?cs else ?>Create<?cs /if ?>" />
+     <input type="submit" name="cancel" value="Cancel" />
+    </div>
+   </form><?cs
+   if:config.exists ?><div class="platforms">
+    <form class="platforms" method="post" action="">
+     <h2>Target Platforms</h2><?cs
+      if:len(config.platforms) ?><ul><?cs
+       each:platform = config.platforms ?>
+        <li><input type="checkbox" name="delete_platform" value="<?cs
+         var:platform.id ?>"> <a href="<?cs
+         var:platform.href ?>"><?cs var:platform.name ?></a>
+        </li><?cs
+       /each ?></ul><?cs
+      /if ?>
+     <div class="buttons">
+      <input type="submit" name="new" value="Add target platform" />
+      <input type="submit" name="delete" value="Delete selected platforms" />
+     </div>
+    </form>
+   </div><?cs
+   /if ?><?cs
+
+  elif:page.mode == 'view_config' ?><ul>
+   <li>Active: <?cs if:config.active ?>yes<?cs else ?>no<?cs /if ?></li>
+   <li>Path: <?cs if:config.path ?><a href="<?cs
+     var:config.browser_href ?>"><?cs
+     var:config.path ?></a></li><?cs /if ?></ul><?cs
+   if:config.description ?><div class="description"><?cs
+     var:config.description ?></div><?cs
+   /if ?><?cs
+   if:config.can_modify ?><div class="buttons">
+    <form method="get" action=""><div>
+     <input type="hidden" name="action" value="edit" />
+     <input type="submit" value="Edit configuration" />
+    </div></form><?cs
+   /if ?><?cs
+   if:len(config.platforms) ?>
+    <table class="listing" id="builds"><thead><tr><th>Changeset</th><?cs
+    each:platform = config.platforms ?><th><?cs var:platform.name ?><?cs
+    /each ?></tr></thead><?cs
+    if:len(config.builds) ?><tbody><?cs
+     each:rev = config.builds ?><tr>
+      <th class="rev" scope="row"><a href="<?cs
+        var:rev.href ?>" title="View Changeset">[<?cs
+        var:name(rev) ?>]</a></th><?cs
+      each:platform = config.platforms ?><?cs
+       if:len(rev[platform.id]) ?><?cs
+        with:build = rev[platform.id] ?><td class="<?cs
+         var:build.cls ?>"><a href="<?cs
+         var:build.href ?>" title="View build results"><?cs
+         var:build.slave.name ?></a> (<?cs
+         var:build.slave.os ?> <?cs
+         var:build.slave.os.version ?>)<br /><?cs
+         if:build.status == 'in progress' ?>started <?cs
+          var:build.started_delta ?> ago<?cs
+         else ?>took <?cs
+          var:build.duration ?></td><?cs
+         /if ?><?cs
+        /with ?><?cs
+       else ?><td>&mdash;</td><?cs
+       /if ?><?cs
+      /each ?></tr><?cs
+     /each ?></tbody><?cs
+    /if ?></table><?cs
+   /if ?></div><?cs
+
+  elif:page.mode == 'edit_platform' ?>
+   <form class="platform" method="post" action="">
+    <div class="field"><label>Name:<br />
+     <input type="text" name="name" value="<?cs var:platform.name ?>" />
+    </label></div>
+    <h2>Rules</h2>
+    <table><thead><tr>
+     <th>Property name</th><th>Match pattern</th>
+    </tr></thead><tbody><?cs
+     each:rule = platform.rules ?><tr>
+      <td><input type="text" name="property_<?cs var:name(rule) ?>" value="<?cs
+       var:rule.property ?>" /></td>
+      <td><input type="text" name="pattern_<?cs var:name(rule) ?>" value="<?cs
+       var:rule.pattern ?>" /></td>
+      <td><input type="submit" name="rm_rule_<?cs
+        var:name(rule) ?>" value="-" /><input type="submit" name="add_rule_<?cs
+        var:name(rule) ?>" value="+" />
+      </td>
+     </tr><?cs /each ?>
+    </tbody></table>
+    <div class="buttons">
+     <form method="get" action=""><div>
+     <input type="hidden" name="action" value="<?cs
+       if:platform.exists ?>edit<?cs else ?>new<?cs /if ?>" />
+      <input type="hidden" name="platform" value="<?cs
+       var:platform.id ?>" />
+      <input type="submit" value="<?cs
+       if:platform.exists ?>Save changes<?cs else ?>Add platform<?cs
+       /if ?>" />
+      <input type="submit" name="cancel" value="Cancel" />
+     </div></form>
+    </div>
+   </form><?cs
+
+  /if ?>
+ </div>
+<?cs include:"footer.cs" ?>
deleted file mode 100644
--- a/bitten/trac_ext/templates/build.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-<?cs include:"header.cs" ?>
- <div id="ctxtnav" class="nav"></div>
- <div id="content" class="build">
-  <h1><?cs var:title ?></h1><?cs
-
-  if:page.mode == 'overview' ?><?cs
-   each:config = configs ?>
-    <h2><a href="<?cs var:config.href ?>"><?cs var:config.label ?></a></h2><?cs
-    if:config.description ?><div class="description"><?cs
-     var:config.description ?></div><?cs
-    /if ?><?cs
-   /each ?><?cs
-   if:config.can_create ?><div class="buttons">
-    <form method="get" action=""><div>
-     <input type="hidden" name="action" value="new" />
-     <input type="submit" value="Add configuration" />
-    </div></form></div><?cs
-   /if ?></div><?cs
-
-  elif:page.mode == 'edit_config' ?>
-   <form class="config" method="post" action="">
-    <table><tr>
-     <td class="name"><label>Name:<br />
-      <input type="text" name="name" value="<?cs var:config.name ?>" />
-     </label></td>
-     <td class="label"><label>Label (for display):<br />
-      <input type="text" name="label" size="32" value="<?cs
-        var:config.label ?>" />
-     </label></td>
-    </tr><tr>
-     <td class="active"><label><input type="checkbox" name="active"<?cs
-       if:config.active ?> checked="checked"<?cs /if ?> /> Active
-     </label></td>
-     <td class="path"><label>Repository path:<br />
-      <input type="text" name="path" size="48" value="<?cs
-        var:config.path ?>" />
-     </label></td>
-    </tr><tr>
-     <td colspan="2"><fieldset class="iefix">
-      <label for="description">Description (you may use <a tabindex="42" href="<?cs
-        var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label>
-      <p><textarea id="description" name="description" class="wikitext" rows="5" cols="78"><?cs
-        var:config.description ?></textarea></p>
-      <script type="text/javascript" src="<?cs
-        var:htdocs_location ?>js/wikitoolbar.js"></script>
-     </fieldset></td>
-    </tr></table>
-    <div class="buttons">
-     <input type="hidden" name="action" value="<?cs
-       if:config.exists ?>edit<?cs else ?>new<?cs /if ?>" />
-     <input type="submit" value="<?cs
-       if:config.exists ?>Save changes<?cs else ?>Create<?cs /if ?>" />
-     <input type="submit" name="cancel" value="Cancel" />
-    </div>
-   </form><?cs
-   if:config.exists ?><div class="platforms">
-    <form class="platforms" method="post" action="">
-     <h2>Target Platforms</h2><?cs
-      if:len(config.platforms) ?><ul><?cs
-       each:platform = config.platforms ?>
-        <li><input type="checkbox" name="delete_platform" value="<?cs
-         var:platform.id ?>"> <a href="<?cs
-         var:platform.href ?>"><?cs var:platform.name ?></a>
-        </li><?cs
-       /each ?></ul><?cs
-      /if ?>
-     <div class="buttons">
-      <input type="submit" name="new" value="Add target platform" />
-      <input type="submit" name="delete" value="Delete selected platforms" />
-     </div>
-    </form>
-   </div><?cs
-   /if ?><?cs
-
-  elif:page.mode == 'view_config' ?><ul>
-   <li>Active: <?cs if:config.active ?>yes<?cs else ?>no<?cs /if ?></li>
-   <li>Path: <?cs if:config.path ?><a href="<?cs
-     var:config.browser_href ?>"><?cs
-     var:config.path ?></a></li><?cs /if ?></ul><?cs
-   if:config.description ?><div class="description"><?cs
-     var:config.description ?></div><?cs
-   /if ?><?cs
-   if:config.can_modify ?><div class="buttons">
-    <form method="get" action=""><div>
-     <input type="hidden" name="action" value="edit" />
-     <input type="submit" value="Edit configuration" />
-    </div></form><?cs
-   /if ?><?cs
-   if:len(config.platforms) ?>
-    <table class="listing" id="builds"><thead><tr><th>Changeset</th><?cs
-    each:platform = config.platforms ?><th><?cs var:platform.name ?><?cs
-    /each ?></tr></thead><?cs
-    if:len(config.builds) ?><tbody><?cs
-     each:rev = config.builds ?><tr>
-      <th class="rev" scope="row"><a href="<?cs
-        var:rev.href ?>" title="View Changeset">[<?cs
-        var:name(rev) ?>]</a></th><?cs
-      each:platform = config.platforms ?><?cs
-       if:len(rev[platform.id]) ?><?cs
-        with:build = rev[platform.id] ?><td class="<?cs
-         var:build.cls ?>"><a href="<?cs
-         var:build.href ?>" title="View build results"><?cs
-         var:build.slave.name ?></a> (<?cs
-         var:build.slave.os ?> <?cs
-         var:build.slave.os.version ?>)<br /><?cs
-         if:build.status == 'in progress' ?>started <?cs
-          var:build.started_delta ?> ago<?cs
-         else ?>took <?cs
-          var:build.duration ?></td><?cs
-         /if ?><?cs
-        /with ?><?cs
-       else ?><td>&mdash;</td><?cs
-       /if ?><?cs
-      /each ?></tr><?cs
-     /each ?></tbody><?cs
-    /if ?></table><?cs
-   /if ?></div><?cs
-
-  elif:page.mode == 'edit_platform' ?>
-   <form class="platform" method="post" action="">
-    <div class="field"><label>Name:<br />
-     <input type="text" name="name" value="<?cs var:platform.name ?>" />
-    </label></div>
-    <h2>Rules</h2>
-    <table><thead><tr>
-     <th>Property name</th><th>Match pattern</th>
-    </tr></thead><tbody><?cs
-     each:rule = platform.rules ?><tr>
-      <td><input type="text" name="property_<?cs var:name(rule) ?>" value="<?cs
-       var:rule.property ?>" /></td>
-      <td><input type="text" name="pattern_<?cs var:name(rule) ?>" value="<?cs
-       var:rule.pattern ?>" /></td>
-      <td><input type="submit" name="rm_rule_<?cs
-        var:name(rule) ?>" value="-" /><input type="submit" name="add_rule_<?cs
-        var:name(rule) ?>" value="+" />
-      </td>
-     </tr><?cs /each ?>
-    </tbody></table>
-    <div class="buttons">
-     <form method="get" action=""><div>
-     <input type="hidden" name="action" value="<?cs
-       if:platform.exists ?>edit<?cs else ?>new<?cs /if ?>" />
-      <input type="hidden" name="platform" value="<?cs
-       var:platform.id ?>" />
-      <input type="submit" value="<?cs
-       if:platform.exists ?>Save changes<?cs else ?>Add platform<?cs
-       /if ?>" />
-      <input type="submit" name="cancel" value="Cancel" />
-     </div></form>
-    </div>
-   </form><?cs
-
-  elif:page.mode == 'view_build' ?>
-   <p class="trigger">Triggered by: Changeset <a href="<?cs
-     var:build.chgset_href ?>">[<?cs var:build.rev ?>]</a> of <a href="<?cs
-     var:build.config.href ?>"><?cs var:build.config.name ?></a></p>
-   <p class="slave">Built by: <strong title="<?cs
-     var:build.slave.ip_address ?>"><?cs var:build.slave.name ?></strong> (<?cs
-     var:build.slave.os ?> <?cs var:build.slave.os.version ?><?cs
-     if:build.slave.machine ?> on <?cs var:build.slave.machine ?><?cs
-     /if ?>)</p>
-   <p class="time">Completed: <?cs var:build.started ?> (<?cs
-     var:build.started_delta ?> ago)<br />Took: <?cs var:build.duration ?></p><?cs
-  each:step = build.steps ?>
-   <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
-     var:step.duration ?>)</h2>
-   <?cs if:len(step.reports) ?>
-    <div class="reports"><h3>Generated Reports</h3><ul>
-     <?cs each:report = step.reports ?>
-      <li class="<?cs var:report.type ?>"><a href="<?cs var:report.href ?>"><?cs
-        var:report.type ?></a></li>
-     <?cs /each ?>
-    </ul></div>
-   <?cs /if ?>
-   <p><?cs var:step.description ?></p>
-   <div class="log"><?cs
-    each:item = step.log ?><code class="<?cs var:item.level ?>"><?cs
-     var:item.message ?></code><br /><?cs
-    /each ?></div><?cs
-  /each ?><?cs
-  /if ?>
-
- </div>
-<?cs include:"footer.cs" ?>
--- a/bitten/trac_ext/tests/web_ui.py
+++ b/bitten/trac_ext/tests/web_ui.py
@@ -7,10 +7,10 @@
 from trac.web.main import Request, RequestDone
 from bitten.model import BuildConfig, TargetPlatform, Build, schema
 from bitten.trac_ext.main import BuildSystem
-from bitten.trac_ext.web_ui import BuildModule
+from bitten.trac_ext.web_ui import BuildConfigController
 
 
-class BuildModuleTestCase(unittest.TestCase):
+class BuildConfigControllerTestCase(unittest.TestCase):
 
     def setUp(self):
         self.env = EnvironmentStub()
@@ -36,7 +36,7 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -49,7 +49,7 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -65,7 +65,7 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -82,7 +82,7 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -94,7 +94,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -114,7 +114,7 @@
                          'description': 'Bla bla'})
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test', redirected_to[0])
@@ -138,7 +138,7 @@
                    args={'action': 'new', 'cancel': '1', 'name': 'test'})
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build', redirected_to[0])
@@ -155,7 +155,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -179,7 +179,7 @@
                          'description': 'Bla bla'})
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/foo', redirected_to[0])
@@ -209,7 +209,7 @@
                    args={'action': 'edit', 'cancel': '1'})
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test', redirected_to[0])
@@ -225,7 +225,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -246,7 +246,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0])
@@ -266,7 +266,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0])
@@ -287,7 +287,7 @@
                    hdf=HDFWrapper(), perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         module.process_request(req)
 
@@ -315,7 +315,7 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0])
@@ -342,14 +342,14 @@
                    perm=PermissionCache(self.env, 'joe'))
         req.hdf['htdocs_location'] = '/htdocs'
 
-        module = BuildModule(self.env)
+        module = BuildConfigController(self.env)
         assert module.match_request(req)
         self.assertRaises(RequestDone, module.process_request, req)
         self.assertEqual('/trac.cgi/build/test?action=edit', redirected_to[0])
 
 
 def suite():
-    return unittest.makeSuite(BuildModuleTestCase, 'test')
+    return unittest.makeSuite(BuildConfigControllerTestCase, 'test')
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -31,35 +31,54 @@
 from trac.wiki import wiki_to_html
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, BuildLog
 from bitten.store import ReportStore
-
-
-class ILogFormatter(Interface):
-    """Extension point interface for components that format build log
-    messages."""
+from bitten.trac_ext.api import ILogFormatter
 
-    def get_formatter(req, build, step, type):
-        """Return a function that gets called for every log message.
-        
-        The function must take two positional arguments, `level` and `message`,
-        and return the formatted message.
-        """
+_status_label = {Build.IN_PROGRESS: 'in progress',
+                 Build.SUCCESS: 'completed',
+                 Build.FAILURE: 'failed'}
+
+def _build_to_hdf(env, req, build):
+    hdf = {'id': build.id, 'name': build.slave, 'rev': build.rev,
+           'status': _status_label[build.status],
+           'cls': _status_label[build.status].replace(' ', '-'),
+           'href': env.href.build(build.config, build.id),
+           'chgset_href': env.href.changeset(build.rev)}
+    if build.started:
+        hdf['started'] = strftime('%x %X', localtime(build.started))
+        hdf['started_delta'] = pretty_timedelta(build.started)
+    if build.stopped:
+        hdf['stopped'] = strftime('%x %X', localtime(build.stopped))
+        hdf['stopped_delta'] = pretty_timedelta(build.stopped)
+        hdf['duration'] = pretty_timedelta(build.stopped, build.started)
+    hdf['slave'] = {
+        'name': build.slave,
+        'ip_address': build.slave_info.get(Build.IP_ADDRESS),
+        'os': build.slave_info.get(Build.OS_NAME),
+        'os.family': build.slave_info.get(Build.OS_FAMILY),
+        'os.version': build.slave_info.get(Build.OS_VERSION),
+        'machine': build.slave_info.get(Build.MACHINE),
+        'processor': build.slave_info.get(Build.PROCESSOR)
+    }
+    return hdf
+
+class BittenChrome(Component):
+    """Provides the Bitten templates and static resources."""
+
+    implements(ITemplateProvider)
+
+    # ITemplatesProvider methods
+
+    def get_htdocs_dir(self):
+        return pkg_resources.resource_filename(__name__, 'htdocs')
+
+    def get_templates_dir(self):
+        return pkg_resources.resource_filename(__name__, 'templates')
 
 
-class BuildModule(Component):
-    """Implements the Bitten web interface."""
-
-    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider,
-               ITemplateProvider)
+class BuildConfigController(Component):
+    """Implements the web interface for build configurations."""
 
-    log_formatters = ExtensionPoint(ILogFormatter)
-
-    _status_label = {Build.IN_PROGRESS: 'in progress',
-                     Build.SUCCESS: 'completed',
-                     Build.FAILURE: 'failed'}
-    _level_label = {BuildLog.DEBUG: 'debug',
-                    BuildLog.INFO: 'info',
-                    BuildLog.WARNING: 'warning',
-                    BuildLog.ERROR: 'error'}
+    implements(INavigationContributor, IRequestHandler)
 
     # INavigationContributor methods
 
@@ -76,12 +95,10 @@
     # IRequestHandler methods
 
     def match_request(self, req):
-        match = re.match(r'/build(?:/([\w.-]+))?(?:/([\d]+))?$', req.path_info)
+        match = re.match(r'/build(?:/([\w.-]+))?$', req.path_info)
         if match:
             if match.group(1):
                 req.args['config'] = match.group(1)
-                if match.group(2):
-                    req.args['id'] = match.group(2)
             return True
 
     def process_request(self, req):
@@ -89,7 +106,6 @@
 
         action = req.args.get('action')
         config = req.args.get('config')
-        id = req.args.get('id')
 
         if req.method == 'POST':
             if config:
@@ -112,9 +128,7 @@
                 if action == 'new':
                     self._do_create_config(req)
         else:
-            if id:
-                self._render_build(req, id)
-            elif config:
+            if config:
                 if action == 'edit':
                     platform_id = req.args.get('platform')
                     if platform_id:
@@ -134,46 +148,8 @@
                 else:
                     self._render_overview(req)
 
-        add_stylesheet(req, 'build.css')
-        return 'build.cs', None
-
-    # ITemplatesProvider methods
-
-    def get_htdocs_dir(self):
-        return pkg_resources.resource_filename(__name__, 'htdocs')
-
-    def get_templates_dir(self):
-        return pkg_resources.resource_filename(__name__, 'templates')
-
-    # ITimelineEventProvider methods
-
-    def get_timeline_filters(self, req):
-        if req.perm.has_permission('BUILD_VIEW'):
-            yield ('build', 'Builds')
-
-    def get_timeline_events(self, req, start, stop, filters):
-        if 'build' in filters:
-            add_stylesheet(req, 'build.css')
-            db = self.env.get_db_cnx()
-            cursor = db.cursor()
-            cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,b.slave,"
-                           "b.stopped,b.status FROM bitten_build AS b"
-                           "  INNER JOIN bitten_config AS c ON (c.name=b.config)"
-                           "  INNER JOIN bitten_platform AS p ON (p.id=b.platform) "
-                           "WHERE b.stopped>=%s AND b.stopped<=%s "
-                           "AND b.status IN (%s, %s) ORDER BY b.stopped",
-                           (start, stop, Build.SUCCESS, Build.FAILURE))
-            event_kinds = {Build.SUCCESS: 'successbuild',
-                           Build.FAILURE: 'failedbuild'}
-            for id, config, label, rev, platform, slave, stopped, status in cursor:
-                title = 'Build of <em>%s [%s]</em> by %s (%s) %s' \
-                        % (escape(label), escape(rev), escape(slave),
-                           escape(platform), self._status_label[status])
-                if req.args.get('format') == 'rss':
-                    href = self.env.abs_href.build(config, id)
-                else:
-                    href = self.env.href.build(config, id)
-                yield event_kinds[status], href, title, stopped, None, ''
+        add_stylesheet(req, 'bitten.css')
+        return 'bitten_config.cs', None
 
     # Internal methods
 
@@ -343,7 +319,7 @@
                 for build in Build.select(self.env, config=config.name, rev=rev):
                     if build.status == Build.PENDING:
                         continue
-                    req.hdf['%s.%s' % (prefix, build.platform)] = self._build_to_hdf(req, build)
+                    req.hdf['%s.%s' % (prefix, build.platform)] = _build_to_hdf(self.env, req, build)
                 if idx > 4:
                     break
         except TracError, e:
@@ -387,47 +363,59 @@
         }
         req.hdf['page.mode'] = 'edit_platform'
 
-    def _render_build(self, req, build_id):
-        build = Build.fetch(self.env, build_id)
+
+class BuildController(Component):
+    """Renders the build page."""
+    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider)
+
+    log_formatters = ExtensionPoint(ILogFormatter)
+
+    _level_label = {BuildLog.DEBUG: 'debug',
+                    BuildLog.INFO: 'info',
+                    BuildLog.WARNING: 'warning',
+                    BuildLog.ERROR: 'error'}
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'build'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/build/([\w.-]+)/([\d]+)', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['config'] = match.group(1)
+                if match.group(2):
+                    req.args['id'] = match.group(2)
+            return True
+
+    def process_request(self, req):
+        req.perm.assert_permission('BUILD_VIEW')
+
+        db = self.env.get_db_cnx()
+        build_id = int(req.args.get('id'))
+        build = Build.fetch(self.env, build_id, db=db)
         assert build, 'Build %s does not exist' % build_id
+
         add_link(req, 'up', self.env.href.build(build.config),
                  'Build Configuration')
         status2title = {Build.SUCCESS: 'Success', Build.FAILURE: 'Failure',
                         Build.IN_PROGRESS: 'In Progress'}
         req.hdf['title'] = 'Build %s - %s' % (build_id,
                                               status2title[build.status])
-        req.hdf['build'] = self._build_to_hdf(req, build, include_output=True)
         req.hdf['page.mode'] = 'view_build'
-
-        config = BuildConfig.fetch(self.env, build.config)
+        config = BuildConfig.fetch(self.env, build.config, db=db)
         req.hdf['build.config'] = {
             'name': config.label,
             'href': self.env.href.build(config.name)
         }
 
-    def _build_to_hdf(self, req, build, include_output=False):
-        hdf = {'id': build.id, 'name': build.slave, 'rev': build.rev,
-               'status': self._status_label[build.status],
-               'cls': self._status_label[build.status].replace(' ', '-'),
-               'href': self.env.href.build(build.config, build.id),
-               'chgset_href': self.env.href.changeset(build.rev)}
-        if build.started:
-            hdf['started'] = strftime('%x %X', localtime(build.started))
-            hdf['started_delta'] = pretty_timedelta(build.started)
-        if build.stopped:
-            hdf['stopped'] = strftime('%x %X', localtime(build.stopped))
-            hdf['stopped_delta'] = pretty_timedelta(build.stopped)
-            hdf['duration'] = pretty_timedelta(build.stopped, build.started)
-        hdf['slave'] = {
-            'name': build.slave,
-            'ip_address': build.slave_info.get(Build.IP_ADDRESS),
-            'os': build.slave_info.get(Build.OS_NAME),
-            'os.family': build.slave_info.get(Build.OS_FAMILY),
-            'os.version': build.slave_info.get(Build.OS_VERSION),
-            'machine': build.slave_info.get(Build.MACHINE),
-            'processor': build.slave_info.get(Build.PROCESSOR)
-        }
-        db = self.env.get_db_cnx()
+        req.hdf['build'] = _build_to_hdf(self.env, req, build)
         steps = []
         for step in BuildStep.select(self.env, build=build.id, db=db):
             steps.append({
@@ -435,33 +423,62 @@
                 'duration': pretty_timedelta(step.started, step.stopped),
                 'failed': step.status == BuildStep.FAILURE
             })
-            if include_output:
-                for log in BuildLog.select(self.env, build=build.id,
-                                           step=step.name, db=db):
-                    formatters = []
-                    items = []
-                    for formatter in self.log_formatters:
-                        formatters.append(formatter.get_formatter(req, build,
-                                                                  step,
-                                                                  log.type))
-                    for level, message in log.messages:
-                        for format in formatters:
-                            message = format(level, message)
-                        items.append({'level': level, 'message': message})
-                    steps[-1]['log'] = items
+            for log in BuildLog.select(self.env, build=build.id,
+                                       step=step.name, db=db):
+                formatters = []
+                items = []
+                for formatter in self.log_formatters:
+                    formatters.append(formatter.get_formatter(req, build,
+                                                              step,
+                                                              log.type))
+                for level, message in log.messages:
+                    for format in formatters:
+                        message = format(level, message)
+                    items.append({'level': level, 'message': message})
+                steps[-1]['log'] = items
 
-                store = ReportStore(self.env)
-                reports = []
-                for report in store.retrieve_reports(build, step):
-                    report_type = report.attr['type']
-                    report_href = self.env.href.buildreport(build.id, step.name,
-                                                            report_type)
-                    reports.append({'type': report_type, 'href': report_href})
-                steps[-1]['reports'] = reports
+            store = ReportStore(self.env)
+            reports = []
+            for report in store.retrieve_reports(build, step):
+                report_type = report.attr['type']
+                report_href = self.env.href.buildreport(build.id, step.name,
+                                                        report_type)
+                reports.append({'type': report_type, 'href': report_href})
+            steps[-1]['reports'] = reports
+        req.hdf['build.steps'] = steps
 
-        hdf['steps'] = steps
+        add_stylesheet(req, 'bitten.css')
+        return 'bitten_build.cs', None
 
-        return hdf
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('BUILD_VIEW'):
+            yield ('build', 'Builds')
+
+    def get_timeline_events(self, req, start, stop, filters):
+        if 'build' in filters:
+            add_stylesheet(req, 'bitten.css')
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT b.id,b.config,c.label,b.rev,p.name,b.slave,"
+                           "b.stopped,b.status FROM bitten_build AS b"
+                           "  INNER JOIN bitten_config AS c ON (c.name=b.config)"
+                           "  INNER JOIN bitten_platform AS p ON (p.id=b.platform) "
+                           "WHERE b.stopped>=%s AND b.stopped<=%s "
+                           "AND b.status IN (%s, %s) ORDER BY b.stopped",
+                           (start, stop, Build.SUCCESS, Build.FAILURE))
+            event_kinds = {Build.SUCCESS: 'successbuild',
+                           Build.FAILURE: 'failedbuild'}
+            for id, config, label, rev, platform, slave, stopped, status in cursor:
+                title = 'Build of <em>%s [%s]</em> by %s (%s) %s' \
+                        % (escape(label), escape(rev), escape(slave),
+                           escape(platform), _status_label[status])
+                if req.args.get('format') == 'rss':
+                    href = self.env.abs_href.build(config, id)
+                else:
+                    href = self.env.href.build(config, id)
+                yield event_kinds[status], href, title, stopped, None, ''
 
 
 class SourceFileLinkFormatter(Component):
@@ -495,7 +512,7 @@
         return _formatter
 
 
-class BuildReportView(Component):
+class BuildReportController(Component):
     """Temporary web interface that simply displays the XML source of a report
     using the Trac `Mimeview` component."""
 
Copyright (C) 2012-2017 Edgewall Software