Mercurial > bitten > bitten-test
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>—</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>—</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."""