aboutsummaryrefslogtreecommitdiffstats
path: root/interfaces
diff options
context:
space:
mode:
authorChris Ball <cjb@laptop.org>2010-06-24 18:58:03 -0400
committerChris Ball <cjb@laptop.org>2010-06-24 18:58:03 -0400
commita461eec26dd3abae18f33df8de7c7185b678e00f (patch)
tree40557241a9dcd79d37ed3fd0b341e7e4c48bcf66 /interfaces
parent9763adba687a0e8921187702dd55e9a7083c9db4 (diff)
parent545f633e0f24443c319d6f1bea82cdb480e0f2a2 (diff)
downloadbugseverywhere-a461eec26dd3abae18f33df8de7c7185b678e00f.tar.gz
Merge branch 'cfbe'
Diffstat (limited to 'interfaces')
-rw-r--r--interfaces/web/.hgignore6
-rw-r--r--interfaces/web/.hgtags2
-rw-r--r--interfaces/web/LICENSE24
-rw-r--r--interfaces/web/README20
-rw-r--r--interfaces/web/__init__.py0
-rwxr-xr-xinterfaces/web/cfbe.py38
-rw-r--r--interfaces/web/static/scripts/jquery.corners.min.js7
-rw-r--r--interfaces/web/static/style/aal.css99
-rw-r--r--interfaces/web/static/style/cfbe.css180
-rw-r--r--interfaces/web/templates/base.html106
-rw-r--r--interfaces/web/templates/bug.html160
-rw-r--r--interfaces/web/templates/list.html27
-rw-r--r--interfaces/web/web.py174
13 files changed, 843 insertions, 0 deletions
diff --git a/interfaces/web/.hgignore b/interfaces/web/.hgignore
new file mode 100644
index 0000000..a0e81b7
--- /dev/null
+++ b/interfaces/web/.hgignore
@@ -0,0 +1,6 @@
+syntax: glob
+*.pyc
+.DS_Store
+*.log
+*.tmproj
+
diff --git a/interfaces/web/.hgtags b/interfaces/web/.hgtags
new file mode 100644
index 0000000..eeea432
--- /dev/null
+++ b/interfaces/web/.hgtags
@@ -0,0 +1,2 @@
+8d8c7f52f3afb6026dd47d7303a7f6a734b3177d alpha
+abfe7aa4bdf3cd019ad1d51278c293a4e008b397 alpha
diff --git a/interfaces/web/LICENSE b/interfaces/web/LICENSE
new file mode 100644
index 0000000..44f0935
--- /dev/null
+++ b/interfaces/web/LICENSE
@@ -0,0 +1,24 @@
+
+copyrev: 566007698e1bb8a4f0bc4929a68ecc068ab28890
+copy: LICENSE.txt
+
+Copyright (c) 2009 Steve Losh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/interfaces/web/README b/interfaces/web/README
new file mode 100644
index 0000000..6bd04e5
--- /dev/null
+++ b/interfaces/web/README
@@ -0,0 +1,20 @@
+-*- markdown -*-
+
+Cherry Flavored Bugs Everywhere
+===============================
+
+CFBE is a quick web interface to [BugsEverywhere](http://bugseverywhere.org/). It's still very much a work-in-progress.
+
+Installing
+----------
+
+I intend to streamline the installation once I'm satisfied with the interface itself. For now, the install process goes something like this:
+
+* Install [CherryPy](http://cherrypy.org/) if you don't have it.
+* Install [Jinja2](http://jinja.pocoo.org/2/) if you don't have it.
+* Install [BugsEverywhere](http://bugseverywhere.org/) if you don't have it.
+* Download a zip/tar of CFBE (or hg clone) from the [Mercurial repository](http://bitbucket.org/sjl/cherryflavoredbugseverywhere/).
+* Unzip (if you grabbed a zip) and put the folder in your Python site-packages directory (or put it anywhere and symlink it to site-packages).
+* Symlink `site-packages/cherryflavoredbugseverywhere/cfbe.py` to `/usr/local/bin/cfbe`
+* Use `cfbe [project_root]` to start up the web interface for that project.
+* Visit http://localhost:8080/ in a browser.
diff --git a/interfaces/web/__init__.py b/interfaces/web/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/interfaces/web/__init__.py
diff --git a/interfaces/web/cfbe.py b/interfaces/web/cfbe.py
new file mode 100755
index 0000000..e8d80ca
--- /dev/null
+++ b/interfaces/web/cfbe.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+import cherrypy
+import web
+from optparse import OptionParser
+from os import path
+
+module_dir = path.dirname(path.abspath(web.__file__))
+template_dir = path.join(module_dir, 'templates')
+
+def build_parser():
+ """Builds and returns the command line option parser."""
+
+ usage = 'usage: %prog bug_directory'
+ parser = OptionParser(usage)
+ return parser
+
+def parse_arguments():
+ """Parse the command line arguments."""
+
+ parser = build_parser()
+ (options, args) = parser.parse_args()
+
+ if len(args) != 1:
+ parser.error('You need to specify a bug directory.')
+
+ return { 'bug_root': args[0], }
+
+
+config = path.join(module_dir, 'cfbe.config')
+options = parse_arguments()
+
+WebInterface = web.WebInterface(path.abspath(options['bug_root']), template_dir)
+
+cherrypy.config.update({'tools.staticdir.root': path.join(module_dir, 'static')})
+app_config = { '/static': { 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '', } }
+cherrypy.quickstart(WebInterface, '/', app_config)
diff --git a/interfaces/web/static/scripts/jquery.corners.min.js b/interfaces/web/static/scripts/jquery.corners.min.js
new file mode 100644
index 0000000..0b2f979
--- /dev/null
+++ b/interfaces/web/static/scripts/jquery.corners.min.js
@@ -0,0 +1,7 @@
+/*
+ * jQuery Corners 0.3
+ * Copyright (c) 2008 David Turnbull, Steven Wittens
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ */
+jQuery.fn.corners=function(C){var N="rounded_by_jQuery_corners";var V=B(C);var F=false;try{F=(document.body.style.WebkitBorderRadius!==undefined);var Y=navigator.userAgent.indexOf("Chrome");if(Y>=0){F=false}}catch(E){}var W=false;try{W=(document.body.style.MozBorderRadius!==undefined);var Y=navigator.userAgent.indexOf("Firefox");if(Y>=0&&parseInt(navigator.userAgent.substring(Y+8))<3){W=false}}catch(E){}return this.each(function(b,h){$e=jQuery(h);if($e.hasClass(N)){return }$e.addClass(N);var a=/{(.*)}/.exec(h.className);var c=a?B(a[1],V):V;var j=h.nodeName.toLowerCase();if(j=="input"){h=O(h)}if(F&&c.webkit){K(h,c)}else{if(W&&c.mozilla&&(c.sizex==c.sizey)){M(h,c)}else{var d=D(h.parentNode);var f=D(h);switch(j){case"a":case"input":Z(h,c,d,f);break;default:R(h,c,d,f);break}}}});function K(d,c){var a=""+c.sizex+"px "+c.sizey+"px";var b=jQuery(d);if(c.tl){b.css("WebkitBorderTopLeftRadius",a)}if(c.tr){b.css("WebkitBorderTopRightRadius",a)}if(c.bl){b.css("WebkitBorderBottomLeftRadius",a)}if(c.br){b.css("WebkitBorderBottomRightRadius",a)}}function M(d,c){var a=""+c.sizex+"px";var b=jQuery(d);if(c.tl){b.css("-moz-border-radius-topleft",a)}if(c.tr){b.css("-moz-border-radius-topright",a)}if(c.bl){b.css("-moz-border-radius-bottomleft",a)}if(c.br){b.css("-moz-border-radius-bottomright",a)}}function Z(k,n,l,a){var m=S("table");var i=S("tbody");m.appendChild(i);var j=S("tr");var d=S("td","top");j.appendChild(d);var h=S("tr");var c=T(k,n,S("td"));h.appendChild(c);var f=S("tr");var b=S("td","bottom");f.appendChild(b);if(n.tl||n.tr){i.appendChild(j);X(d,n,l,a,true)}i.appendChild(h);if(n.bl||n.br){i.appendChild(f);X(b,n,l,a,false)}k.appendChild(m);if(jQuery.browser.msie){m.onclick=Q}k.style.overflow="hidden"}function Q(){if(!this.parentNode.onclick){this.parentNode.click()}}function O(c){var b=document.createElement("a");b.id=c.id;b.className=c.className;if(c.onclick){b.href="javascript:";b.onclick=c.onclick}else{jQuery(c).parent("form").each(function(){b.href=this.action});b.onclick=I}var a=document.createTextNode(c.value);b.appendChild(a);c.parentNode.replaceChild(b,c);return b}function I(){jQuery(this).parent("form").each(function(){this.submit()});return false}function R(d,a,b,c){var f=T(d,a,document.createElement("div"));d.appendChild(f);if(a.tl||a.tr){X(d,a,b,c,true)}if(a.bl||a.br){X(d,a,b,c,false)}}function T(j,i,k){var b=jQuery(j);var l;while(l=j.firstChild){k.appendChild(l)}if(j.style.height){var f=parseInt(b.css("height"));k.style.height=f+"px";f+=parseInt(b.css("padding-top"))+parseInt(b.css("padding-bottom"));j.style.height=f+"px"}if(j.style.width){var a=parseInt(b.css("width"));k.style.width=a+"px";a+=parseInt(b.css("padding-left"))+parseInt(b.css("padding-right"));j.style.width=a+"px"}k.style.paddingLeft=b.css("padding-left");k.style.paddingRight=b.css("padding-right");if(i.tl||i.tr){k.style.paddingTop=U(j,i,b.css("padding-top"),true)}else{k.style.paddingTop=b.css("padding-top")}if(i.bl||i.br){k.style.paddingBottom=U(j,i,b.css("padding-bottom"),false)}else{k.style.paddingBottom=b.css("padding-bottom")}j.style.padding=0;return k}function U(f,a,d,c){if(d.indexOf("px")<0){try{console.error("%s padding not in pixels",(c?"top":"bottom"),f)}catch(b){}d=a.sizey+"px"}d=parseInt(d);if(d-a.sizey<0){try{console.error("%s padding is %ipx for %ipx corner:",(c?"top":"bottom"),d,a.sizey,f)}catch(b){}d=a.sizey}return d-a.sizey+"px"}function S(b,a){var c=document.createElement(b);c.style.border="none";c.style.borderCollapse="collapse";c.style.borderSpacing=0;c.style.padding=0;c.style.margin=0;if(a){c.style.verticalAlign=a}return c}function D(b){try{var d=jQuery.css(b,"background-color");if(d.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i)&&b.parentNode){return D(b.parentNode)}if(d==null){return"#ffffff"}if(d.indexOf("rgb")>-1){d=A(d)}if(d.length==4){d=L(d)}return d}catch(a){return"#ffffff"}}function L(a){return"#"+a.substring(1,2)+a.substring(1,2)+a.substring(2,3)+a.substring(2,3)+a.substring(3,4)+a.substring(3,4)}function A(h){var a=255;var d="";var b;var e=/([0-9]+)[, ]+([0-9]+)[, ]+([0-9]+)/;var f=e.exec(h);for(b=1;b<4;b++){d+=("0"+parseInt(f[b]).toString(16)).slice(-2)}return"#"+d}function B(b,d){var b=b||"";var c={sizex:5,sizey:5,tl:false,tr:false,bl:false,br:false,webkit:true,mozilla:true,transparent:false};if(d){c.sizex=d.sizex;c.sizey=d.sizey;c.webkit=d.webkit;c.transparent=d.transparent;c.mozilla=d.mozilla}var a=false;var e=false;jQuery.each(b.split(" "),function(f,j){j=j.toLowerCase();var h=parseInt(j);if(h>0&&j==h+"px"){c.sizey=h;if(!a){c.sizex=h}a=true}else{switch(j){case"no-native":c.webkit=c.mozilla=false;break;case"webkit":c.webkit=true;break;case"no-webkit":c.webkit=false;break;case"mozilla":c.mozilla=true;break;case"no-mozilla":c.mozilla=false;break;case"anti-alias":c.transparent=false;break;case"transparent":c.transparent=true;break;case"top":e=c.tl=c.tr=true;break;case"right":e=c.tr=c.br=true;break;case"bottom":e=c.bl=c.br=true;break;case"left":e=c.tl=c.bl=true;break;case"top-left":e=c.tl=true;break;case"top-right":e=c.tr=true;break;case"bottom-left":e=c.bl=true;break;case"bottom-right":e=c.br=true;break}}});if(!e){if(!d){c.tl=c.tr=c.bl=c.br=true}else{c.tl=d.tl;c.tr=d.tr;c.bl=d.bl;c.br=d.br}}return c}function P(f,d,h){var e=Array(parseInt("0x"+f.substring(1,3)),parseInt("0x"+f.substring(3,5)),parseInt("0x"+f.substring(5,7)));var c=Array(parseInt("0x"+d.substring(1,3)),parseInt("0x"+d.substring(3,5)),parseInt("0x"+d.substring(5,7)));r="0"+Math.round(e[0]+(c[0]-e[0])*h).toString(16);g="0"+Math.round(e[1]+(c[1]-e[1])*h).toString(16);d="0"+Math.round(e[2]+(c[2]-e[2])*h).toString(16);return"#"+r.substring(r.length-2)+g.substring(g.length-2)+d.substring(d.length-2)}function X(f,a,b,d,c){if(a.transparent){G(f,a,b,c)}else{J(f,a,b,d,c)}}function J(k,z,p,a,n){var h,f;var l=document.createElement("div");l.style.fontSize="1px";l.style.backgroundColor=p;var b=0;for(h=1;h<=z.sizey;h++){var u,t,q;arc=Math.sqrt(1-Math.pow(1-h/z.sizey,2))*z.sizex;var c=z.sizex-Math.ceil(arc);var w=Math.floor(b);var v=z.sizex-c-w;var o=document.createElement("div");var m=l;o.style.margin="0px "+c+"px";o.style.height="1px";o.style.overflow="hidden";for(f=1;f<=v;f++){if(f==1){if(f==v){u=((arc+b)*0.5)-w}else{t=Math.sqrt(1-Math.pow(1-(c+1)/z.sizex,2))*z.sizey;u=(t-(z.sizey-h))*(arc-w-v+1)*0.5}}else{if(f==v){t=Math.sqrt(1-Math.pow((z.sizex-c-f+1)/z.sizex,2))*z.sizey;u=1-(1-(t-(z.sizey-h)))*(1-(b-w))*0.5}else{q=Math.sqrt(1-Math.pow((z.sizex-c-f)/z.sizex,2))*z.sizey;t=Math.sqrt(1-Math.pow((z.sizex-c-f+1)/z.sizex,2))*z.sizey;u=((t+q)*0.5)-(z.sizey-h)}}H(z,o,m,n,P(p,a,u));m=o;var o=m.cloneNode(false);o.style.margin="0px 1px"}H(z,o,m,n,a);b=arc}if(n){k.insertBefore(l,k.firstChild)}else{k.appendChild(l)}}function H(c,a,e,d,b){if(d&&!c.tl){a.style.marginLeft=0}if(d&&!c.tr){a.style.marginRight=0}if(!d&&!c.bl){a.style.marginLeft=0}if(!d&&!c.br){a.style.marginRight=0}a.style.backgroundColor=b;if(d){e.appendChild(a)}else{e.insertBefore(a,e.firstChild)}}function G(c,o,l,h){var f=document.createElement("div");f.style.fontSize="1px";var a=document.createElement("div");a.style.overflow="hidden";a.style.height="1px";a.style.borderColor=l;a.style.borderStyle="none solid";var m=o.sizex-1;var j=o.sizey-1;if(!j){j=1}for(var b=0;b<o.sizey;b++){var n=m-Math.floor(Math.sqrt(1-Math.pow(1-b/j,2))*m);if(b==2&&o.sizex==6&&o.sizey==6){n=2}var k=a.cloneNode(false);k.style.borderWidth="0 "+n+"px";if(h){k.style.borderWidth="0 "+(o.tr?n:0)+"px 0 "+(o.tl?n:0)+"px"}else{k.style.borderWidth="0 "+(o.br?n:0)+"px 0 "+(o.bl?n:0)+"px"}h?f.appendChild(k):f.insertBefore(k,f.firstChild)}if(h){c.insertBefore(f,c.firstChild)}else{c.appendChild(f)}}}; \ No newline at end of file
diff --git a/interfaces/web/static/style/aal.css b/interfaces/web/static/style/aal.css
new file mode 100644
index 0000000..9bad98f
--- /dev/null
+++ b/interfaces/web/static/style/aal.css
@@ -0,0 +1,99 @@
+/*
+ aardvark.legs by Anatoli Papirovski - http://fecklessmind.com/
+ Licensed under the MIT license. http://www.opensource.org/licenses/mit-license.php
+*/
+
+/*
+ Reset first. Modified version of Eric Meyer and Paul Chaplin reset
+ from http://meyerweb.com/eric/tools/css/reset/
+*/
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+header, nav, section, article, aside, footer
+{border: 0; margin: 0; outline: 0; padding: 0; background: transparent; vertical-align: baseline;}
+
+blockquote, q {quotes: none;}
+blockquote:before,blockquote:after,q:before,q:after {content: ''; content: none;}
+
+header, nav, section, article, aside, footer {display: block;}
+
+/* Basic styles */
+body {background: #fff; color: #000; font: 0.875em/1.5em "Helvetica Neue", Helvetica, Arial, "Liberation Sans", "Bitstream Vera Sans", sans-serif;}
+html>body {font-size: 14px;}
+
+img {display: inline-block; vertical-align: bottom;}
+
+h1,h2,h3,h4,h5,h6,strong,b,dt,th {font-weight: 700;}
+address,cite,em,i,caption,dfn,var {font-style: italic;}
+
+h1 {margin: 0 0 0.75em; font-size: 2em;}
+h2 {margin: 0 0 1em; font-size: 1.5em;}
+h3 {margin: 0 0 1.286em; font-size: 1.167em;}
+h4 {margin: 0 0 1.5em; font-size: 1em;}
+h5 {margin: 0 0 1.8em; font-size: .834em;}
+h6 {margin: 0 0 2em; font-size: .75em;}
+
+p,ul,ol,dl,blockquote,pre {margin: 0 0 1.5em;}
+
+li ul,li ol {margin: 0;}
+ul {list-style: outside disc;}
+ol {list-style: outside decimal;}
+li {margin: 0 0 0 2em;}
+dd {padding-left: 1.5em;}
+blockquote {padding: 0 1.5em;}
+
+a {text-decoration: underline;}
+a:hover {text-decoration: none;}
+abbr,acronym {border-bottom: 1px dotted; cursor: help;}
+del {text-decoration: line-through;}
+ins {text-decoration: overline;}
+sub {font-size: .834em; line-height: 1em; vertical-align: sub;}
+sup {font-size: .834em; line-height: 1em; vertical-align: super;}
+
+tt,code,kbd,samp,pre {font-size: 1em; font-family: "Courier New", Courier, monospace;}
+
+/* Table styles */
+table {border-collapse: collapse; border-spacing: 0; margin: 0 0 1.5em;}
+caption {text-align: left;}
+th, td {padding: .25em .5em;}
+tbody td, tbody th {border: 1px solid #000;}
+tfoot {font-style: italic;}
+
+/* Form styles */
+fieldset {clear: both;}
+legend {padding: 0 0 1.286em; font-size: 1.167em; font-weight: 700;}
+fieldset fieldset legend {padding: 0 0 1.5em; font-size: 1em;}
+* html legend {margin-left: -7px;}
+*+html legend {margin-left: -7px;}
+
+form .field, form .buttons {clear: both; margin: 0 0 1.5em;}
+form .field label {display: block;}
+form ul.fields li {list-style-type: none; margin: 0;}
+form ul.inline li, form ul.inline label {display: inline;}
+form ul.inline li {padding: 0 .75em 0 0;}
+
+input.radio, input.checkbox {vertical-align: top;}
+label, button, input.submit, input.image {cursor: pointer;}
+* html input.radio, * html input.checkbox {vertical-align: middle;}
+*+html input.radio, *+html input.checkbox {vertical-align: middle;}
+
+textarea {overflow: auto;}
+input.text, input.password, textarea, select {margin: 0; font: 1em/1.3 Helvetica, Arial, "Liberation Sans", "Bitstream Vera Sans", sans-serif; vertical-align: bottom;}
+input.text, input.password, textarea {border: 1px solid #444; border-bottom-color: #666; border-right-color: #666; padding: 2px;}
+
+* html button {margin: 0 .34em 0 0;}
+*+html button {margin: 0 .34em 0 0;}
+
+form.horizontal .field {padding-left: 150px;}
+form.horizontal .field label {display: inline; float: left; width: 140px; margin-left: -150px;}
+
+/* Useful classes */
+img.left {display: inline; float: left; margin: 0 1.5em .75em 0;}
+img.right {display: inline; float: right; margin: 0 0 .75em .75em;} \ No newline at end of file
diff --git a/interfaces/web/static/style/cfbe.css b/interfaces/web/static/style/cfbe.css
new file mode 100644
index 0000000..c5f726e
--- /dev/null
+++ b/interfaces/web/static/style/cfbe.css
@@ -0,0 +1,180 @@
+/* @override http://localhost:8080/static/style/cfbe.css */
+
+body {
+ background-color: #eee;
+}
+
+div#main-pane {
+ width: 960px;
+ margin: 3em auto;
+ border: 1px solid #888;
+ background-color: #fcfcfc;
+}
+.inside-main-pane {
+ padding: 0em 3em;
+}
+
+div#header {
+ background-color: #D8004A;
+ height: 6em;
+}
+div#header h1 {
+ font-size: 4em;
+ line-height: 1.5em;
+ margin-bottom: 0em;
+ color: #fff;
+ font-weight: normal;
+ font-family: "Helvetica Neue Ultra Light", "HelveticaNeue-UltraLight", "Helvetica", "Arial", sans-serif;
+ letter-spacing: 1px;
+}
+
+div#navigation {
+ height: 3em;
+ line-height: 3em;
+ border-bottom: 1px solid #888;
+}
+div#content-pane {
+ margin: 1.5em 0em 3em;
+}
+
+div#filter-pane {
+ display: none;
+ border-bottom: 1px solid #888;
+ line-height: 3em;
+ text-align: right;
+}
+ul.filter-items {
+ list-style-type: none;
+ margin: 0em;
+ padding: 0em;
+}
+ul.filter-items li {
+ display: inline;
+ margin-left: 1.5em;
+}
+
+div#footer {
+ text-align: center;
+ height: 3em;
+ border-top: 1px solid #888;
+}
+div#footer p {
+ font-size: 0.9em;
+ line-height: 3.333em;
+}
+
+span#filters {
+ float: right;
+}
+span#filters a {
+ margin-left: 1.5em;
+}
+
+a:link, a:visited, a:active {
+ color: #d03; text-decoration: none; font-weight: bold;
+}
+a:hover {
+ color: #60b305;
+}
+
+.header-with-link {
+ display: inline-block;
+}
+.header-link {
+ margin-left: 1em;
+}
+
+table#bug-list {
+ width: 100%; border-collapse: collapse; border: 0.084em solid #ccc;
+}
+table#bug-list td, table#bug-list th {
+ border: 0em; border-bottom: 0.084em solid #ccc; text-align: left;
+}
+table tr td, table tr th {
+ padding: 0px 5px;
+}
+table tr td {
+ line-height: 2.832em; padding-bottom: 0.084em;
+}
+table tr th {
+ line-height: 2.916em;
+}
+table {
+ margin-bottom: 1.417em;
+}
+tr.stripe {
+ background-color: #fcecf8;
+}
+
+div#assignees, div#targets {
+ display: none;
+}
+
+p.creation-info {
+ color: #888;
+}
+span.detail-field-header {
+ font-weight: 700;
+ width: 7.5em;
+ padding-right: 1em;
+ display: inline-block;
+ text-align: right;
+}
+
+div.bug-comment {
+ margin-left: 2em;
+}
+p.bug-comment-body {
+ white-space: pre;
+ margin: 0em 0em 0em 0em;
+}
+p.bug-comment-footer {
+ margin: 0em 0em; color: #888;
+}
+h4.bug-comment-header {
+ margin: 1.5em 0em 0em;
+}
+
+#create-form {
+ display: none;
+}
+#create-form fieldset {
+ clear: none;
+}
+#create-form input#create-summary {
+ width: 20em;
+ border: 1px solid #888;
+ margin-right: 1.5em;
+}
+#create-button {
+ margin: 0em;
+}
+
+form#add-comment-form {
+ display: none;
+ margin-top: 1.5em;
+}
+p#add-comment-link {
+ margin-top: 1.5em;
+}
+
+form#bug-details-edit-form {
+ display: none;
+}
+form#bug-details-edit-form label {
+ font-weight: 700;
+ width: 7.5em;
+ margin-left: 0em;
+ margin-right: 1em;
+ text-align: right;
+}
+form#bug-details-edit-form .field {
+ padding-left: 0em;
+}
+
+form#bug-summary-edit-form {
+ display: none;
+}
+input#bug-summary-edit-body {
+ width: 95%;
+}
diff --git a/interfaces/web/templates/base.html b/interfaces/web/templates/base.html
new file mode 100644
index 0000000..8f22d73
--- /dev/null
+++ b/interfaces/web/templates/base.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html>
+ <head>
+ <title>Cherry Flavored Bugs Everywhere!</title>
+
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="/static/style/aal.css" />
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="/static/style/cfbe.css" />
+
+ <script type="text/javascript"
+ src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js">
+ </script>
+
+ <script type="text/javascript"
+ src="/static/scripts/jquery.corners.min.js">
+ </script>
+
+ <script type="text/javascript">
+ $(function() {
+ $('#filter-assignee').click(function(e) {
+ $('#filter-pane').html($('#assignees').html());
+ $('#filter-pane').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('#filter-target').click(function(e) {
+ $('#filter-pane').html($('#targets').html());
+ $('#filter-pane').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('#create-bug').click(function(e) {
+ $('#create-bug').hide();
+ $('#create-form').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('table tr:odd').addClass('stripe');
+ });
+ </script>
+
+ {% block script %}{% endblock %}
+ </head>
+
+ <body>
+ <div id="main-pane">
+ <div id="header" class="inside-main-pane">
+ <h1>{{ repository_name }}</h1>
+ </div>
+ <div id="navigation" class="inside-main-pane">
+ <span id="filters">
+ <a href="/">Open</a>
+ <a href="/?status=closed">Closed</a>
+ <a href="" id="filter-assignee">Assigned to...</a>
+ <a href="" id="filter-target">Scheduled for...</a>
+ </span>
+ <span id="create">
+ <a href="" id="create-bug">&#43; Create a new bug</a>
+ </span>
+ <span id="create-form">
+ <form action="/create" method="post">
+ <fieldset>
+ <input type="text"
+ id="create-summary" name="summary" />
+ <button id="create-button"
+ type="submit">Create</button>
+ </fieldset>
+ </form>
+ </span>
+ </div>
+ <div id="filter-pane" class="inside-main-pane"></div>
+ <div id="content-pane" class="inside-main-pane">
+ <h2>{% block page_title %}&nbsp;{% endblock %}</h2>
+ {% block content %}{% endblock %}
+ </div>
+ <div id="footer" class="inside-main-pane">
+ <p>
+ <a href="">Cherry Flavored Bugs Everywhere</a>
+ was created by <a href="http://stevelosh.com">Steve Losh</a> and a very nice <a href="http://fecklessmind.com/2009/01/20/aardvark-css-framework/">aardvark</a>
+ using <a href="http://cherrypy.org">CherryPy</a>,
+ <a href="http://jinja.pocoo.org/2/">Jinja2</a>,
+ and <a href="http://jquery.com">jQuery</a>.
+ </p>
+ </div>
+ </div>
+ <div id="assignees">
+ <ul class="filter-items">
+ <li><a href="/?assignee=None">Unassigned</a></li>
+ {% for assignee in assignees %}
+ <li><a href="/?assignee={{ assignee|e }}">{{ assignee|e }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div id="targets">
+ <ul class="filter-items">
+ <li><a href="/?target=None">Unscheduled</a></li>
+ {% for target in targets %}
+ <li><a href="/?target={{ target }}">{{ target }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </body>
+</html>
diff --git a/interfaces/web/templates/bug.html b/interfaces/web/templates/bug.html
new file mode 100644
index 0000000..66993de
--- /dev/null
+++ b/interfaces/web/templates/bug.html
@@ -0,0 +1,160 @@
+{% extends "base.html" %}
+
+{% block page_title %}
+ Bug {{ bug.id.user() }} &ndash; {{ bug.summary|truncate(70) }}
+{% endblock %}
+
+{% block script %}
+ <script type="text/javascript">
+ $(function() {
+ function set_current_detail_default_values() {
+ $('#bug-details-edit-status option[value="{{ bug.status }}"]').attr('selected', 'yes');
+ $('#bug-details-edit-target option[value="{{ bug.target|e }}"]').attr('selected', 'yes');
+ $('#bug-details-edit-assignee option[value^="{{ bug.assigned|striptags }}"]').attr('selected', 'yes');
+ $('#bug-details-edit-severity option[value="{{ bug.severity }}"]').attr('selected', 'yes');
+ }
+
+ $('#add-comment').click(function(e) {
+ $('#add-comment-link').hide();
+ $('#add-comment-form').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('#edit-bug-details').click(function(e) {
+ $('#bug-details').hide();
+ $('#bug-details-edit-form').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('#bug-details-edit-form button[type="reset"]').click(function(e) {
+ $('#bug-details-edit-form').hide();
+ $('#bug-details').fadeIn('fast', function() { set_current_detail_default_values(); } );
+ });
+
+ $('#edit-bug-summary').click(function(e) {
+ $('#bug-summary').hide();
+ $('#bug-summary-edit-form').fadeIn('fast');
+ e.preventDefault();
+ });
+
+ $('#bug-summary-edit-form button[type="reset"]').click(function(e) {
+ $('#bug-summary-edit-form').hide();
+ $('#bug-summary').fadeIn('fast', function() { set_current_detail_default_values(); } );
+ });
+
+ set_current_detail_default_values();
+ });
+ </script>
+{% endblock %}
+
+{% block content %}
+ <p class="creation-info">Created on {{ bug.time|datetimeformat }} by {{ bug.creator|e }}</p>
+
+ <h3 class="header-with-link">Bug Details</h3>
+ <span class="header-link">
+ <a href="" id="edit-bug-details">edit</a>
+ </span>
+
+ <p id="bug-details">
+ <span class="detail-field-header">Status:</span>
+ <span class="detail-field-contents">{{ bug.status }}</span><br />
+
+ <span class="detail-field-header">Severity:</span>
+ <span class="detail-field-contents">{{ bug.severity }}</span><br />
+
+ <span class="detail-field-header">Scheduled for:</span>
+ <span class="detail-field-contents">{{ target }}</span><br />
+
+ <span class="detail-field-header">Assigned to:</span>
+ <span class="detail-field-contents">{{ assignee|e }}</span><br />
+
+ <span class="detail-field-header">Permanent ID:</span>
+ <span class="detail-field-contents">{{ bug.uuid }}</span><br />
+ </p>
+
+ <form id="bug-details-edit-form" class="horizontal" action="/edit" method="post">
+ <fieldset>
+ <input type="hidden" name="id" value="{{ bug.uuid }}" />
+ <div class="field">
+ <label for="bug-details-edit-status">Status:</label>
+ <select id="bug-details-edit-status" name="status">
+ {% for status in statuses %}
+ <option value="{{ status }}">{{ status }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div class="field">
+ <label for="bug-details-edit-severity">Severity:</label>
+ <select id="bug-details-edit-severity" name="severity">
+ {% for severity in severities %}
+ <option value="{{ severity }}">{{ severity }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div class="field">
+ <label for="bug-details-edit-target">Scheduled for:</label>
+ <select id="bug-details-edit-target" name="target">
+ <option value="None">Unscheduled</option>
+ {% for target in targets %}
+ <option value="{{ target|e }}">{{ target }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div class="field">
+ <label for="bug-details-edit-assignee">Assigned to:</label>
+ <select id="bug-details-edit-assignee" name="assignee">
+ <option value="None">Unassigned</option>
+ {% for assignee in assignees %}
+ <option value="{{ assignee|e }}">{{ assignee|e }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div class="buttons">
+ <button type="submit">Save Changes</button>
+ <button type="reset">Discard Changes</button>
+ </div>
+ </fieldset>
+ </form>
+
+ <h3 class="header-with-link">Summary</h3>
+ <span class="header-link">
+ <a href="" id="edit-bug-summary">edit</a>
+ </span>
+ <p id="bug-summary">
+ {{ bug.summary }}
+ </p>
+
+ <form id="bug-summary-edit-form" class="vertical" action="/edit" method="post">
+ <fieldset>
+ <input type="hidden" name="id" value="{{ bug.uuid }}" />
+ <div class="field">
+ <input type="text" class="text" id="bug-summary-edit-body" name="summary" value="{{ bug.summary }}" />
+ </div>
+ <div class="buttons">
+ <button type="submit">Save Changes</button>
+ <button type="reset">Discard Changes</button>
+ </div>
+ </fieldset>
+ </form>
+
+ <h3>Comments</h3>
+ {% for comment in bug.comments() %}
+ <div class="bug-comment">
+ <h4 class="bug-comment-header">{{ comment.From|striptags|e }} said:</h4>
+ <p class="bug-comment-body">{{ comment.body|trim|e }}</p>
+ <p class="bug-comment-footer">on {{ comment.time|datetimeformat }}</p>
+ </div>
+ {% endfor %}
+ <form id="add-comment-form" class="vertical" action="/comment" method="post">
+ <fieldset>
+ <input type="hidden" name="id" value="{{ bug.uuid }}" />
+ <div class="field">
+ <textarea cols="60" rows="6" id="add-comment-body" name="body"></textarea>
+ </div>
+ <div class="buttons">
+ <button type="submit">Submit</button>
+ </div>
+ </fieldset>
+ </form>
+ <p id="add-comment-link"><a href="" id="add-comment">&#43; Add a comment</a></p>
+{% endblock %}
diff --git a/interfaces/web/templates/list.html b/interfaces/web/templates/list.html
new file mode 100644
index 0000000..83007d3
--- /dev/null
+++ b/interfaces/web/templates/list.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block page_title %}
+ {{ label }}
+{% endblock %}
+
+{% block content %}
+ <table id="bug-list">
+ <tr>
+ <th>ID</th>
+ <th>Summary</th>
+ <th>Status</th>
+ <th>Target</th>
+ <th>Assigned To</th>
+ </tr>
+ {% for bug in bugs %}
+ <tr>
+ <td>{{ bug.id.user() }}</td>
+ <td><a href="/bug?id={{ bug.id.user() }}">
+ {{ bug.summary|e|truncate(70) }}</a></td>
+ <td>{{ bug.status }}</td>
+ <td>{{ bug.target }}</td>
+ <td>{{ bug.assigned|striptags }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+{% endblock %}
diff --git a/interfaces/web/web.py b/interfaces/web/web.py
new file mode 100644
index 0000000..e80f676
--- /dev/null
+++ b/interfaces/web/web.py
@@ -0,0 +1,174 @@
+import cherrypy
+from libbe import storage
+from libbe import bugdir
+from libbe.command.depend import get_blocks
+from libbe.command.util import bug_comment_from_user_id
+from libbe.storage.util import settings_object
+from jinja2 import Environment, FileSystemLoader
+from datetime import datetime
+
+EMPTY = settings_object.EMPTY
+
+def datetimeformat(value, format='%B %d, %Y at %I:%M %p'):
+ """Takes a timestamp and revormats it into a human-readable string."""
+ return datetime.fromtimestamp(value).strftime(format)
+
+
+class WebInterface:
+ """The web interface to CFBE."""
+
+ def __init__(self, bug_root, template_root):
+ """Initialize the bug repository for this web interface."""
+ self.bug_root = bug_root
+ store = storage.get_storage(self.bug_root)
+ store.connect()
+ version = store.storage_version()
+ print version
+ self.bd = bugdir.BugDir(store, from_storage=True)
+ self.repository_name = self.bug_root.split('/')[-1]
+ self.env = Environment(loader=FileSystemLoader(template_root))
+ self.env.filters['datetimeformat'] = datetimeformat
+
+ def get_common_information(self):
+ """Returns a dict of common information that most pages will need."""
+ possible_assignees = list(set(
+ [unicode(bug.assigned) for bug in self.bd if bug.assigned != EMPTY]))
+ possible_assignees.sort(key=unicode.lower)
+
+ possible_targets = list(set(
+ [unicode(bug.summary.rstrip("\n")) for bug in self.bd \
+ if bug.severity == u"target"]))
+
+ possible_targets.sort(key=unicode.lower)
+
+ possible_statuses = [u'open', u'assigned', u'test', u'unconfirmed',
+ u'closed', u'disabled', u'fixed', u'wontfix']
+
+ possible_severities = [u'minor', u'serious', u'critical', u'fatal',
+ u'wishlist']
+
+ return {'possible_assignees': possible_assignees,
+ 'possible_targets': possible_targets,
+ 'possible_statuses': possible_statuses,
+ 'possible_severities': possible_severities,
+ 'repository_name': self.repository_name,}
+
+ def filter_bugs(self, status, assignee, target):
+ """Filter the list of bugs to return only those desired."""
+ bugs = [bug for bug in self.bd if bug.status in status]
+
+ if assignee != '':
+ assignee = EMPTY if assignee == 'None' else assignee
+ bugs = [bug for bug in bugs if bug.assigned == assignee]
+
+ if target != '':
+ target = None if target == 'None' else target
+ bugs = [bug for bug in bugs if bug.target == target]
+
+ return bugs
+
+
+ @cherrypy.expose
+ def index(self, status='open', assignee='', target=''):
+ """The main bug page.
+ Bugs can be filtered by assignee or target.
+ The bug database will be reloaded on each visit."""
+
+ self.bd.load_all_bugs()
+
+ if status == 'open':
+ status = ['open', 'assigned', 'test', 'unconfirmed', 'wishlist']
+ label = 'All Open Bugs'
+ elif status == 'closed':
+ status = ['closed', 'disabled', 'fixed', 'wontfix']
+ label = 'All Closed Bugs'
+
+ if assignee != '':
+ label += ' Currently Unassigned' if assignee == 'None' \
+ else ' Assigned to %s' % (assignee,)
+ if target != '':
+ label += ' Currently Unschdeuled' if target == 'None' \
+ else ' Scheduled for %s' % (target,)
+
+ template = self.env.get_template('list.html')
+ bugs = self.filter_bugs(status, assignee, target)
+
+ common_info = self.get_common_information()
+ return template.render(bugs=bugs, bd=self.bd, label=label,
+ assignees=common_info['possible_assignees'],
+ targets=common_info['possible_targets'],
+ statuses=common_info['possible_statuses'],
+ severities=common_info['possible_severities'],
+ repository_name=common_info['repository_name'])
+
+
+ @cherrypy.expose
+ def bug(self, id=''):
+ """The page for viewing a single bug."""
+
+ self.bd.load_all_bugs()
+
+ bug, comment = bug_comment_from_user_id(self.bd, id)
+
+ template = self.env.get_template('bug.html')
+ common_info = self.get_common_information()
+
+ # Determine which targets a bug has.
+ # First, is this bug blocking any other bugs?
+ targets = ''
+ blocks = get_blocks(self.bd, bug)
+ for targetbug in blocks:
+ # Are any of those blocked bugs targets?
+ blocker = self.bd.bug_from_uuid(targetbug.uuid)
+ if blocker.severity == "target":
+ targets += "%s " % blocker.summary
+
+ return template.render(bug=bug, bd=self.bd,
+ assignee='' if bug.assigned == EMPTY else bug.assigned,
+ target=targets,
+ assignees=common_info['possible_assignees'],
+ targets=common_info['possible_targets'],
+ statuses=common_info['possible_statuses'],
+ severities=common_info['possible_severities'],
+ repository_name=common_info['repository_name'])
+
+
+ @cherrypy.expose
+ def create(self, summary):
+ """The view that handles the creation of a new bug."""
+ if summary.strip() != '':
+ self.bd.new_bug(summary=summary).save()
+ raise cherrypy.HTTPRedirect('/', status=302)
+
+
+ @cherrypy.expose
+ def comment(self, id, body):
+ """The view that handles adding a comment."""
+ bug = self.bd.bug_from_uuid(id)
+ shortname = self.bd.bug_shortname(bug)
+
+ if body.strip() != '':
+ bug.comment_root.new_reply(body=body)
+ bug.save()
+
+ raise cherrypy.HTTPRedirect('/bug?id=%s' % (shortname,), status=302)
+
+
+ @cherrypy.expose
+ def edit(self, id, status=None, target=None, assignee=None, severity=None, summary=None):
+ """The view that handles editing bug details."""
+ bug = self.bd.bug_from_uuid(id)
+ shortname = self.bd.bug_shortname(bug)
+
+ if summary != None:
+ bug.summary = summary
+ else:
+ bug.status = status if status != 'None' else None
+ bug.target = target if target != 'None' else None
+ bug.assigned = assignee if assignee != 'None' else None
+ bug.severity = severity if severity != 'None' else None
+
+ bug.save()
+
+ raise cherrypy.HTTPRedirect('/bug?id=%s' % (shortname,), status=302)
+