diff options
author | Chris Ball <cjb@laptop.org> | 2010-06-24 18:58:03 -0400 |
---|---|---|
committer | Chris Ball <cjb@laptop.org> | 2010-06-24 18:58:03 -0400 |
commit | a461eec26dd3abae18f33df8de7c7185b678e00f (patch) | |
tree | 40557241a9dcd79d37ed3fd0b341e7e4c48bcf66 /interfaces | |
parent | 9763adba687a0e8921187702dd55e9a7083c9db4 (diff) | |
parent | 545f633e0f24443c319d6f1bea82cdb480e0f2a2 (diff) | |
download | bugseverywhere-a461eec26dd3abae18f33df8de7c7185b678e00f.tar.gz |
Merge branch 'cfbe'
Diffstat (limited to 'interfaces')
-rw-r--r-- | interfaces/web/.hgignore | 6 | ||||
-rw-r--r-- | interfaces/web/.hgtags | 2 | ||||
-rw-r--r-- | interfaces/web/LICENSE | 24 | ||||
-rw-r--r-- | interfaces/web/README | 20 | ||||
-rw-r--r-- | interfaces/web/__init__.py | 0 | ||||
-rwxr-xr-x | interfaces/web/cfbe.py | 38 | ||||
-rw-r--r-- | interfaces/web/static/scripts/jquery.corners.min.js | 7 | ||||
-rw-r--r-- | interfaces/web/static/style/aal.css | 99 | ||||
-rw-r--r-- | interfaces/web/static/style/cfbe.css | 180 | ||||
-rw-r--r-- | interfaces/web/templates/base.html | 106 | ||||
-rw-r--r-- | interfaces/web/templates/bug.html | 160 | ||||
-rw-r--r-- | interfaces/web/templates/list.html | 27 | ||||
-rw-r--r-- | interfaces/web/web.py | 174 |
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">+ 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 %} {% 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() }} – {{ 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">+ 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) + |