'+b.html+' '),d=a("#"+c).find("a").first();return d.click(e(function(a){return this.choice_destroy_link_click(a)},this))},b.prototype.choice_destroy_link_click=function(b){b.preventDefault(),this.pending_destroy_click=!0;return this.choice_destroy(a(b.target))},b.prototype.choice_destroy=function(a){this.choices-=1,this.show_search_field_default(),this.is_multiple&&this.choices>0&&this.search_field.val().length<1&&this.results_hide(),this.result_deselect(a.attr("rel"));return a.parents("li").first().remove()},b.prototype.result_select=function(){var a,b,c,d;if(this.result_highlight){a=this.result_highlight,b=a.attr("id"),this.result_clear_highlight(),a.addClass("result-selected"),this.is_multiple?this.result_deactivate(a):this.result_single_selected=a,d=b.substr(b.lastIndexOf("_")+1),c=this.results_data[d],c.selected=!0,this.form_field.options[c.options_index].selected=!0,this.is_multiple?this.choice_build(c):this.selected_item.find("span").first().text(c.text),this.results_hide(),this.search_field.val(""),this.form_field_jq.trigger("change");return this.search_field_scale()}},b.prototype.result_activate=function(a){return a.addClass("active-result").show()},b.prototype.result_deactivate=function(a){return a.removeClass("active-result").hide()},b.prototype.result_deselect=function(b){var c,d;d=this.results_data[b],d.selected=!1,this.form_field.options[d.options_index].selected=!1,c=a("#"+this.container_id+"_o_"+b),c.removeClass("result-selected").addClass("active-result").show(),this.result_clear_highlight(),this.winnow_results(),this.form_field_jq.trigger("change");return this.search_field_scale()},b.prototype.results_search=function(a){return this.results_showing?this.winnow_results():this.results_show()},b.prototype.winnow_results=function(){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r;j=new Date,this.no_results_clear(),h=0,i=this.search_field.val()===this.default_text?"":a("
").text(a.trim(this.search_field.val())).html(),f=new RegExp("^"+i.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),"i"),m=new RegExp(i.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),"i"),r=this.results_data;for(n=0,p=r.length;n=0||c.html.indexOf("[")===0){e=c.html.replace(/\[|\]/g,"").split(" ");if(e.length)for(o=0,q=e.length;o"+c.html.substr(k+i.length),l=l.substr(0,k)+""+l.substr(k)):l=c.html,a("#"+g).html!==l&&a("#"+g).html(l),this.result_activate(a("#"+g)),c.group_array_index!=null&&a("#"+this.results_data[c.group_array_index].dom_id).show()):(this.result_highlight&&g===this.result_highlight.attr("id")&&this.result_clear_highlight(),this.result_deactivate(a("#"+g)))}}return h<1&&i.length?this.no_results(i):this.winnow_results_set_highlight()},b.prototype.winnow_results_clear=function(){var b,c,d,e,f;this.search_field.val(""),c=this.search_results.find("li"),f=[];for(d=0,e=c.length;dNo results match " "'),c.find("span").first().html(b);return this.search_results.append(c)},b.prototype.no_results_clear=function(){return this.search_results.find(".no-results").remove()},b.prototype.keydown_arrow=function(){var b,c;this.result_highlight?this.results_showing&&(c=this.result_highlight.nextAll("li.active-result").first(),c&&this.result_do_highlight(c)):(b=this.search_results.find("li.active-result").first(),b&&this.result_do_highlight(a(b)));if(!this.results_showing)return this.results_show()},b.prototype.keyup_arrow=function(){var a;if(!this.results_showing&&!this.is_multiple)return this.results_show();if(this.result_highlight){a=this.result_highlight.prevAll("li.active-result");if(a.length)return this.result_do_highlight(a.first());this.choices>0&&this.results_hide();return this.result_clear_highlight()}},b.prototype.keydown_backstroke=function(){if(this.pending_backstroke){this.choice_destroy(this.pending_backstroke.find("a").first());return this.clear_backstroke()}this.pending_backstroke=this.search_container.siblings("li.search-choice").last();return this.pending_backstroke.addClass("search-choice-focus")},b.prototype.clear_backstroke=function(){this.pending_backstroke&&this.pending_backstroke.removeClass("search-choice-focus");return this.pending_backstroke=null},b.prototype.keyup_checker=function(a){var b,c;b=(c=a.which)!=null?c:a.keyCode,this.search_field_scale();switch(b){case 8:if(this.is_multiple&&this.backstroke_length<1&&this.choices>0)return this.keydown_backstroke();if(!this.pending_backstroke){this.result_clear_highlight();return this.results_search()}break;case 13:a.preventDefault();if(this.results_showing)return this.result_select();break;case 27:if(this.results_showing)return this.results_hide();break;case 9:case 38:case 40:case 16:break;default:return this.results_search()}},b.prototype.keydown_checker=function(a){var b,c;b=(c=a.which)!=null?c:a.keyCode,this.search_field_scale(),b!==8&&this.pending_backstroke&&this.clear_backstroke();switch(b){case 8:this.backstroke_length=this.search_field.val().length;break;case 9:this.mouse_on_container=!1;break;case 13:a.preventDefault();break;case 38:a.preventDefault(),this.keyup_arrow();break;case 40:this.keydown_arrow()}},b.prototype.search_field_scale=function(){var b,c,d,e,f,g,h,i,j;if(this.is_multiple){d=0,h=0,f="position:absolute; left: -1000px; top: -1000px; display:none;",g=["font-size","font-style","font-weight","font-family","line-height","text-transform","letter-spacing"];for(i=0,j=g.length;i ",{style:f}),c.text(this.search_field.val()),a("body").append(c),h=c.width()+25,c.remove(),h>this.f_width-10&&(h=this.f_width-10),this.search_field.css({width:h+"px"}),b=this.container.height();return this.dropdown.css({top:b+"px"})}},b.prototype.generate_field_id=function(){var a;a=this.generate_random_id(),this.form_field.id=a;return a},b.prototype.generate_random_id=function(){var b;b="sel"+this.generate_random_char()+this.generate_random_char()+this.generate_random_char();while(a("#"+b).length>0)b+=this.generate_random_char();return b},b.prototype.generate_random_char=function(){var a,b,c;a="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZ",c=Math.floor(Math.random()*a.length);return b=a.substring(c,c+1)};return b}(),c=function(a){var b;return b=a.outerWidth()-a.width()},d.get_side_border_padding=c}).call(this),function(){var a;a=function(){function a(){this.options_index=0,this.parsed=[]}a.prototype.add_node=function(a){return a.nodeName==="OPTGROUP"?this.add_group(a):this.add_option(a)},a.prototype.add_group=function(a){var b,c,d,e,f,g;b=this.parsed.length,this.parsed.push({array_index:b,group:!0,label:a.label,children:0,disabled:a.disabled}),f=a.childNodes,g=[];for(d=0,e=f.length;d .
3 | * Author: Drew Diller
4 | * Email: drew.diller@gmail.com
5 | * URL: http://www.dillerdesign.com/experiment/DD_belatedPNG/
6 | * Version: 0.0.8a
7 | * Licensed under the MIT License: http://dillerdesign.com/experiment/DD_belatedPNG/#license
8 | *
9 | * Example usage:
10 | * DD_belatedPNG.fix('.png_bg'); // argument is a CSS selector
11 | * DD_belatedPNG.fixPng( someNode ); // argument is an HTMLDomElement
12 | **/
13 | var DD_belatedPNG={ns:"DD_belatedPNG",imgSize:{},delay:10,nodesFixed:0,createVmlNameSpace:function(){if(document.namespaces&&!document.namespaces[this.ns]){document.namespaces.add(this.ns,"urn:schemas-microsoft-com:vml")}},createVmlStyleSheet:function(){var b,a;b=document.createElement("style");b.setAttribute("media","screen");document.documentElement.firstChild.insertBefore(b,document.documentElement.firstChild.firstChild);if(b.styleSheet){b=b.styleSheet;b.addRule(this.ns+"\\:*","{behavior:url(#default#VML)}");b.addRule(this.ns+"\\:shape","position:absolute;");b.addRule("img."+this.ns+"_sizeFinder","behavior:none; border:none; position:absolute; z-index:-1; top:-10000px; visibility:hidden;");this.screenStyleSheet=b;a=document.createElement("style");a.setAttribute("media","print");document.documentElement.firstChild.insertBefore(a,document.documentElement.firstChild.firstChild);a=a.styleSheet;a.addRule(this.ns+"\\:*","{display: none !important;}");a.addRule("img."+this.ns+"_sizeFinder","{display: none !important;}")}},readPropertyChange:function(){var b,c,a;b=event.srcElement;if(!b.vmlInitiated){return}if(event.propertyName.search("background")!=-1||event.propertyName.search("border")!=-1){DD_belatedPNG.applyVML(b)}if(event.propertyName=="style.display"){c=(b.currentStyle.display=="none")?"none":"block";for(a in b.vml){if(b.vml.hasOwnProperty(a)){b.vml[a].shape.style.display=c}}}if(event.propertyName.search("filter")!=-1){DD_belatedPNG.vmlOpacity(b)}},vmlOpacity:function(b){if(b.currentStyle.filter.search("lpha")!=-1){var a=b.currentStyle.filter;a=parseInt(a.substring(a.lastIndexOf("=")+1,a.lastIndexOf(")")),10)/100;b.vml.color.shape.style.filter=b.currentStyle.filter;b.vml.image.fill.opacity=a}},handlePseudoHover:function(a){setTimeout(function(){DD_belatedPNG.applyVML(a)},1)},fix:function(a){if(this.screenStyleSheet){var c,b;c=a.split(",");for(b=0;bn.H){i.B=n.H}d.vml.image.shape.style.clip="rect("+i.T+"px "+(i.R+a)+"px "+i.B+"px "+(i.L+a)+"px)"}else{d.vml.image.shape.style.clip="rect("+f.T+"px "+f.R+"px "+f.B+"px "+f.L+"px)"}},figurePercentage:function(d,c,f,a){var b,e;e=true;b=(f=="X");switch(a){case"left":case"top":d[f]=0;break;case"center":d[f]=0.5;break;case"right":case"bottom":d[f]=1;break;default:if(a.search("%")!=-1){d[f]=parseInt(a,10)/100}else{e=false}}d[f]=Math.ceil(e?((c[b?"W":"H"]*d[f])-(c[b?"w":"h"]*d[f])):parseInt(a,10));if(d[f]%2===0){d[f]++}return d[f]},fixPng:function(c){c.style.behavior="none";var g,b,f,a,d;if(c.nodeName=="BODY"||c.nodeName=="TD"||c.nodeName=="TR"){return}c.isImg=false;if(c.nodeName=="IMG"){if(c.src.toLowerCase().search(/\.png$/)!=-1){c.isImg=true;c.style.visibility="hidden"}else{return}}else{if(c.currentStyle.backgroundImage.toLowerCase().search(".png")==-1){return}}g=DD_belatedPNG;c.vml={color:{},image:{}};b={shape:{},fill:{}};for(a in c.vml){if(c.vml.hasOwnProperty(a)){for(d in b){if(b.hasOwnProperty(d)){f=g.ns+":"+d;c.vml[a][d]=document.createElement(f)}}c.vml[a].shape.stroked=false;c.vml[a].shape.appendChild(c.vml[a].fill);c.parentNode.insertBefore(c.vml[a].shape,c)}}c.vml.image.shape.fillcolor="none";c.vml.image.fill.type="tile";c.vml.color.fill.on=false;g.attachHandlers(c);g.giveLayout(c);g.giveLayout(c.offsetParent);c.vmlInitiated=true;g.applyVML(c)}};try{document.execCommand("BackgroundImageCache",false,true)}catch(r){}DD_belatedPNG.createVmlNameSpace();DD_belatedPNG.createVmlStyleSheet();
--------------------------------------------------------------------------------
/dashboard/static/js/libs/modernizr-1.7.min.js:
--------------------------------------------------------------------------------
1 | // Modernizr v1.7 www.modernizr.com
2 | window.Modernizr=function(a,b,c){function G(){e.input=function(a){for(var b=0,c=a.length;b7)},r.history=function(){return !!(a.history&&history.pushState)},r.draganddrop=function(){return x("dragstart")&&x("drop")},r.websockets=function(){return"WebSocket"in a},r.rgba=function(){A("background-color:rgba(150,255,150,.5)");return D(k.backgroundColor,"rgba")},r.hsla=function(){A("background-color:hsla(120,40%,100%,.5)");return D(k.backgroundColor,"rgba")||D(k.backgroundColor,"hsla")},r.multiplebgs=function(){A("background:url(//:),url(//:),red url(//:)");return(new RegExp("(url\\s*\\(.*?){3}")).test(k.background)},r.backgroundsize=function(){return F("backgroundSize")},r.borderimage=function(){return F("borderImage")},r.borderradius=function(){return F("borderRadius","",function(a){return D(a,"orderRadius")})},r.boxshadow=function(){return F("boxShadow")},r.textshadow=function(){return b.createElement("div").style.textShadow===""},r.opacity=function(){B("opacity:.55");return/^0.55$/.test(k.opacity)},r.cssanimations=function(){return F("animationName")},r.csscolumns=function(){return F("columnCount")},r.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";A((a+o.join(b+a)+o.join(c+a)).slice(0,-a.length));return D(k.backgroundImage,"gradient")},r.cssreflections=function(){return F("boxReflect")},r.csstransforms=function(){return!!E(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"])},r.csstransforms3d=function(){var a=!!E(["perspectiveProperty","WebkitPerspective","MozPerspective","OPerspective","msPerspective"]);a&&"webkitPerspective"in g.style&&(a=w("@media ("+o.join("transform-3d),(")+"modernizr)"));return a},r.csstransitions=function(){return F("transitionProperty")},r.fontface=function(){var a,c,d=h||g,e=b.createElement("style"),f=b.implementation||{hasFeature:function(){return!1}};e.type="text/css",d.insertBefore(e,d.firstChild),a=e.sheet||e.styleSheet;var i=f.hasFeature("CSS2","")?function(b){if(!a||!b)return!1;var c=!1;try{a.insertRule(b,0),c=/src/i.test(a.cssRules[0].cssText),a.deleteRule(a.cssRules.length-1)}catch(d){}return c}:function(b){if(!a||!b)return!1;a.cssText=b;return a.cssText.length!==0&&/src/i.test(a.cssText)&&a.cssText.replace(/\r+|\n+/g,"").indexOf(b.split(" ")[0])===0};c=i('@font-face { font-family: "font"; src: url(data:,); }'),d.removeChild(e);return c},r.video=function(){var a=b.createElement("video"),c=!!a.canPlayType;if(c){c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"');var d='video/mp4; codecs="avc1.42E01E';c.h264=a.canPlayType(d+'"')||a.canPlayType(d+', mp4a.40.2"'),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"')}return c},r.audio=function(){var a=b.createElement("audio"),c=!!a.canPlayType;c&&(c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"'),c.mp3=a.canPlayType("audio/mpeg;"),c.wav=a.canPlayType('audio/wav; codecs="1"'),c.m4a=a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;"));return c},r.localstorage=function(){try{return!!localStorage.getItem}catch(a){return!1}},r.sessionstorage=function(){try{return!!sessionStorage.getItem}catch(a){return!1}},r.webWorkers=function(){return!!a.Worker},r.applicationcache=function(){return!!a.applicationCache},r.svg=function(){return!!b.createElementNS&&!!b.createElementNS(q.svg,"svg").createSVGRect},r.inlinesvg=function(){var a=b.createElement("div");a.innerHTML=" ";return(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.smil=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"animate")))},r.svgclippaths=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"clipPath")))};for(var H in r)z(r,H)&&(v=H.toLowerCase(),e[v]=r[H](),u.push((e[v]?"":"no-")+v));e.input||G(),e.crosswindowmessaging=e.postmessage,e.historymanagement=e.history,e.addTest=function(a,b){a=a.toLowerCase();if(!e[a]){b=!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b;return e}},A(""),j=l=null,f&&a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML=" ";return a.childNodes.length!==1}()&&function(a,b){function p(a,b){var c=-1,d=a.length,e,f=[];while(++c this.colorArray[j].condition) {
159 | e.features[i].element.setAttribute("stroke", this.colorArray[i].color);
160 | }
161 | }
162 | }
163 | }
164 | }
165 | };
166 |
--------------------------------------------------------------------------------
/dashboard/static/js/plugins.js:
--------------------------------------------------------------------------------
1 |
2 | // usage: log('inside coolFunc', this, arguments);
3 | // paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
4 | window.log = function(){
5 | log.history = log.history || []; // store logs to an array for reference
6 | log.history.push(arguments);
7 | arguments.callee = arguments.callee.caller;
8 | if(this.console) console.log( Array.prototype.slice.call(arguments) );
9 | };
10 | // make it safe to use console.log always
11 | (function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();)b[a]=b[a]||c})(window.console=window.console||{});
12 |
13 |
14 | // place any jQuery/helper plugins in here, instead of separate, slower script files.
15 |
16 |
--------------------------------------------------------------------------------
/dashboard/static/js/raycasting.js:
--------------------------------------------------------------------------------
1 | var features_coordinates = []; //dom ready
2 | var features_rect_bounds = [];
3 | var neighborhood_features = [];
4 | var highlighted = -1;
5 |
6 | var RayCaster = {
7 | insideRectBounds: function (feature_rect_bounds,mp_ll) {
8 | var lon = mp_ll[0],
9 | lat = mp_ll[1],
10 | max_lon = feature_rect_bounds[0],
11 | min_lon = feature_rect_bounds[1],
12 | max_lat = feature_rect_bounds[2],
13 | min_lat = feature_rect_bounds[3];
14 |
15 | if ((lon > max_lon) || (lon < min_lon) ||
16 | (lat > max_lat) || (lat < min_lat)) {
17 | return false;
18 | } else {
19 | return true;
20 | }
21 | },
22 |
23 | insidePolygon: function (mp_ll, index) {
24 | if (!this.insideRectBounds(features_rect_bounds[index],mp_ll)){
25 | return false;
26 | }
27 |
28 | var verticesCount = features_coordinates[index].length;
29 | var inside = false;
30 | var i;
31 | var j = verticesCount - 1;
32 | var vertexA;
33 | var vertexB;
34 |
35 | for(i=0; i < verticesCount; i++){
36 | vertexA = features_coordinates[index][i];
37 | vertexB = features_coordinates[index][j];
38 |
39 | if(((vertexA[0] < mp_ll[0]) && (vertexB[0] >= mp_ll[0])) || ((vertexB[0] < mp_ll[0]) && (vertexA[0] >= mp_ll[0]))) {
40 | if(vertexA[1] + (((mp_ll[0] - vertexA[0]) / (vertexB[0] - vertexA[0])) * (vertexB[1] - vertexA[1])) < mp_ll[1]) {
41 | inside = !inside;
42 | }
43 | }
44 | j = i;
45 | }
46 |
47 | return inside;
48 | }
49 | };
50 |
51 | function onloadneighborhoods(e){
52 | var lon = [],
53 | lat = [],
54 | i;
55 |
56 | for(i = 0; i < e.features.length; i++) {
57 |
58 | neighborhood_features[i] = e.features[i];
59 | neighborhood_features[i].element.setAttribute('fill','#fff');
60 | neighborhood_features[i].element.setAttribute('fill-opacity','0');
61 | neighborhood_features[i].element.setAttribute('id', "neighbrohood-"+e.features[i].data.properties.id);
62 |
63 | features_coordinates[i] = e.features[i].data.geometry.coordinates[0];
64 |
65 | for (var j=0; j < e.features[i].data.geometry.coordinates[0].length; j++){
66 | lon[j] = e.features[i].data.geometry.coordinates[0][j][0];
67 | lat[j] = e.features[i].data.geometry.coordinates[0][j][1];
68 | }
69 | features_rect_bounds[i] = [Math.max.apply(Math,lon),Math.min.apply(Math,lon),Math.max.apply(Math,lat),Math.min.apply(Math,lat)];
70 |
71 | e.features[i].element.onmouseover = testNeighborhood;
72 | e.features[i].element.onmousemove = testNeighborhood;
73 | e.features[i].element.onmouseout = testNeighborhood;
74 |
75 | e.features[i].element.onclick = function () {
76 | id = this.getAttribute('id');
77 | id = id.split('-');
78 | window.location = "/neighborhood/"+id[1]+"/";
79 | };
80 | }
81 |
82 | }
83 |
84 | function testNeighborhood(e){
85 | var mp_ll = [Map.map.pointLocation(Map.map.mouse(e)).lon,
86 | Map.map.pointLocation(Map.map.mouse(e)).lat];
87 |
88 | for(var i = 0; i < features_coordinates.length; i++){
89 | if (RayCaster.insidePolygon(mp_ll,i)){
90 | handleHighlight(i);
91 | return;
92 | }
93 | }
94 |
95 | handleHighlight(-1);
96 | };
97 |
98 | function handleHighlight(i){
99 | if (highlighted === i){
100 | return;
101 | } else if (highlighted !== -1){
102 | unhighlightNeighborhood(highlighted);
103 | };
104 |
105 | if (i !== -1){
106 | highlightNeighborhood(i);
107 | }
108 | highlighted = i;
109 | console.log('highlighted',highlighted);
110 |
111 | };
112 | var count = 0;
113 |
114 | function highlightNeighborhood(i){
115 | neighborhood_features[i].element.setAttribute('style','stroke-width:1;stroke:#050505;stroke-opacity:1;fill:#fff;fill-opacity:.4;');
116 | }
117 |
118 | function unhighlightNeighborhood(i){
119 | neighborhood_features[i].element.setAttribute('style','stroke-opacity:0;fill-opacity:0;');
120 | };
121 |
122 |
123 | function onload(e){
124 | var colorArray = ['#D92B04','#A61103'];
125 |
126 | for(var i = 0; i < e.features.length; i++) {
127 | //new
128 | e.features[i].element.setAttribute('fill-opacity',0);
129 |
130 | var streetMouseOver = function(score,start_street,end_street,street,months,top_request,index){
131 | alert("Over!");
132 | return function(evt){
133 | setStreetContent(evt,score,start_street,end_street,street,months,top_request,index);
134 | };
135 | }(e.features[i].data.properties.score,e.features[i].data.properties.RT_FADD,e.features[i].data.properties.RT_TOADD,e.features[i].data.properties.STREETN_GC,e.features[i].data.properties.months,e.features[i].data.properties.top_request_type,i);
136 |
137 | e.features[i].element.onmouseover = //streetMouseOver;
138 | e.features[i].element.onmouseout = hideStreetContent;
139 |
140 |
141 | if (e.features[i].data.properties.score < 600){
142 | e.features[i].element.setAttribute("stroke",colorArray[0]);
143 | e.features[i].element.setAttribute("stroke-opacity", 0.75);
144 | } else {
145 | e.features[i].element.setAttribute("stroke",colorArray[1]);
146 | e.features[i].element.setAttribute("stroke-opacity", 0.8);
147 | }
148 |
149 |
150 | e.features[i].element.setAttribute("stroke-linecap","round");
151 | }
152 | }
153 |
154 | function setStreetContent(e,score,start_street,end_street,street,months,top_request,index){
155 | testNeighborhood(e);
156 | }
157 |
158 | function hideStreetContent(e){
159 | testNeighborhood(e);
160 | }
161 |
162 | function onresponseload(e){
163 | var colorArray = ['#23677f','#15343f'];
164 |
165 | for(var i = 0; i < e.features.length; i++) {
166 |
167 | e.features[i].element.setAttribute('fill-opacity',0);
168 |
169 | var streetMouseOver = function(score,start_street,end_street,street,index){
170 | return function(evt){
171 | setResponseContent(evt,score,start_street,end_street,street,index);
172 | };
173 | }(e.features[i].data.properties.response_time,e.features[i].data.properties.RT_FADD,e.features[i].data.properties.RT_TOADD,e.features[i].data.properties.STREETN_GC,i);
174 |
175 | e.features[i].element.onmouseover = streetMouseOver;
176 | e.features[i].element.onmouseout = hideResponseContent;
177 |
178 |
179 | if (e.features[i].data.properties.average < 480){
180 | e.features[i].element.setAttribute("stroke",colorArray[0]);
181 | e.features[i].element.setAttribute("stroke-opacity", 0.65);
182 | } else {
183 | e.features[i].element.setAttribute("stroke",colorArray[1]);
184 | e.features[i].element.setAttribute("stroke-opacity", 0.7);
185 | }
186 |
187 |
188 | e.features[i].element.setAttribute("stroke-linecap","round");
189 | }
190 |
191 | }
192 |
193 | function onsidewalkload(e) {
194 | var colorArray = ['rgb(21,52,63)','rgb(35,103,127)'];
195 | for (var i = 0; i < e.features.length; i++) {
196 | if (e.features[i].data.properties.percentile > .9) {
197 | e.features[i].element.setAttribute("stroke", colorArray[0]);
198 | e.features[i].element.setAttribute("stroke-opacity", 0.7);
199 | } else if (e.features[i].data.properties.percentile > .8) {
200 | e.features[i].element.setAttribute("stroke", colorArray[0]);
201 | e.features[i].element.setAttribute("stroke-opacity", .65);
202 | }
203 | e.features[i].element.setAttribute("stroke-linecap", "round");
204 | e.features[i].element.setAttribute("fill", "none");
205 | e.features[i].element.setAttribute("id", "street-"+e.features[i].data.properties.id);
206 | e.features[i].element.onclick = function () {
207 | id = this.getAttribute('id');
208 | id = id.split('-');
209 | window.location = "/street/"+id[1]+"/";
210 | };
211 | }
212 | }
213 |
214 | function setResponseContent(e,score,start_street,end_street,street){
215 | // testNeighborhood(e);
216 | }
217 |
218 | function hideResponseContent(e){
219 | // testNeighborhood(e);
220 | }
221 |
222 | Map.createmap();
223 |
224 | aws_url = "http://open311-tiles.s3-website-us-east-1.amazonaws.com/";
225 | zxy = "/{Z}/{X}/{Y}.png";
226 |
227 | var cleaning = Map.addLayer(aws_url+"speed-cleaning"+zxy,
228 | { type: "image", index:1 });
229 | var cans = Map.addLayer(aws_url+"speed-cans"+zxy,
230 | { type: "image", index:1 });
231 | var dumping = Map.addLayer(aws_url+"speed-dumping"+zxy,
232 | { type: "image", index:1 });
233 | var graffiti = Map.addLayer(aws_url+"speed-graffiti.final"+zxy,
234 | { type: "image", index:1, visible:true });
235 |
236 | var overflowing = Map.addLayer(aws_url+"speed-overflowing"+zxy,
237 | { type: "image", index:1 });
238 | var pavementdefect = Map.addLayer(aws_url+"speed-pavementdefect"+zxy,
239 | { type: "image", index:1 });
240 | var sewer = Map.addLayer(aws_url+"speed-sewer"+zxy,
241 | { type: "image", index:1 });
242 | var sidewalk = Map.addLayer(aws_url+"speed-sidewalk"+zxy,
243 | { type: "image", index:1 });
244 | var vehicle = Map.addLayer(aws_url+"speed-vehicle"+zxy,
245 | { type: "image", index:1 });
246 |
247 |
248 | Map.addLayer("/static/neighborhoods.json",
249 | { id: "neighborhoods",
250 | load: onloadneighborhoods,
251 | zoom: 14,
252 | alwaysVisible: true, index:5});
253 |
254 | Map.addLayer("/static/graffiti.json",
255 | { id: "graffiti",
256 | load: onsidewalkload,
257 | group: graffiti, index: 10, visible:true });
258 |
259 | /*
260 | var context_map = po.image()
261 | .url(po.url("http://{S}tile.cloudmade.com"
262 | + "/1a193057ca6040fca68c4ae162bec2da"
263 | + "/38965/256/{Z}/{X}/{Y}.png")
264 | .hosts(["a.", "b.", "c.", ""]));
265 | map.add(context_map);
266 | context_map.visible(false);
267 |
268 | var response_lines = po.geoJson()
269 | .url("/static/test.json")
270 | .id("responses")
271 | .zoom(12)
272 | .tile(false)
273 | .on("load", onresponseload
274 | );
275 |
276 | map.add(response_lines);
277 | // response_lines.visible(false);
278 |
279 | map.on("move", function(){if (map.zoom() >= 14) {
280 | context_map.visible(true);
281 | } else {
282 | context_map.visible(false);
283 | }});*/
284 |
285 |
--------------------------------------------------------------------------------
/dashboard/static/js/script.js:
--------------------------------------------------------------------------------
1 | /* Author: Chris Barna */
2 | /* global barchart */
3 | $(function () {
4 | 'use strict';
5 |
6 | var pad = function (number, length) {
7 | var str = number.toString();
8 | while (str.length < length) {
9 | str = '0' + str;
10 | }
11 | return str;
12 | }, renderBarchart = function (fromDate, toDate) {
13 | $.ajax({
14 | url: '/api/tickets/both/' + fromDate.getFullYear() + '-' + pad(fromDate.getMonth() + 1, 2) + '-' + pad(fromDate.getDate(), 2) + '/' + toDate.getFullYear() + '-' + pad(toDate.getMonth() + 1, 2) + '-' + pad(toDate.getDate(), 2) + '/',
15 | dataType: 'json',
16 | success: function (data) {
17 | barchart.render(fromDate.getTime(), toDate.getTime(), data);
18 | }
19 | });
20 | };
21 |
22 |
23 | $("#from, #to").datepicker({
24 | changeMonth: true,
25 | onSelect: function () {
26 | $("#chart").html('');
27 | renderBarchart($('#from').datepicker('getDate'), $('#to').datepicker('getDate'));
28 | }
29 | });
30 |
31 | $('#to').datepicker('setDate', new Date());
32 | $('#from').datepicker('setDate', -28);
33 |
34 | renderBarchart($("#from").datepicker('getDate'), $("#to").datepicker('getDate'));
35 |
36 | });
37 |
38 |
--------------------------------------------------------------------------------
/dashboard/static/js/sparkline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
55 |
56 |
57 | Animated Sparkline
58 |
59 |
60 |
61 |
62 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/dashboard/templates/admin/city_view.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/admin.html' %}
2 | {% load humanize %}
3 |
4 | {% block content %}
5 | {{city.name}}
6 |
7 |
8 | URL: {{ city.url }}
9 |
10 |
11 |
12 | More data
13 |
14 |
15 | Requests : {{ requests|intcomma }}
16 | Geographies : {{ geographies|intcomma }}
17 | Streets : {{ streets|intcomma }}
18 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/dashboard/templates/admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/admin.html' %}
2 |
3 | {% block content %}
4 | Manage Cities (+ )
5 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/dashboard/templates/base/admin.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
--------------------------------------------------------------------------------
/dashboard/templates/base/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 | {% block page_title %}Open311 Dashboard{% endblock %}
14 |
15 |
16 |
17 | {# #}
18 | {# #}
19 |
20 |
21 | {# #}
22 | {# #}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
59 |
60 |
61 | {% block content %}{% endblock %}
62 |
63 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {% block custom_scripts %}{% endblock %}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/dashboard/templates/geo_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
28 |
29 |
30 |
31 |
32 |
Open Requests
33 | {% if stats.open_requests|length > 0 %}
34 |
35 | {% for request in stats.open_requests %}
36 |
37 | {{ request.get_service_name }}
38 | {{ request.requested_datetime }}
39 |
40 | {% endfor %}
41 |
42 | {% else %}
43 |
No requests currently open.
44 | {% endif %}
45 |
46 |
47 |
48 |
{{ title|title }}
49 | {% if neighborhood %}
50 | {{ neighborhood.name }}
51 |
{% endif %}
52 |
53 |
30-Day Trend
54 |
{{ stats.average_response }}
55 | day{{ stats.average_response|pluralize }}
56 |
Average Response Time
57 |
58 |
59 |
Top Requests
60 |
61 | {% for type in stats.request_types %}
62 |
63 | {{ type.service_name }}
64 | {{ type.count }}
65 |
66 | {% endfor %}
67 |
68 |
69 |
JSON Export
70 |
71 |
81 |
82 |
83 |
84 |
91 | {% endblock %}
92 |
93 | {% block custom_scripts %}
94 |
95 | {% endblock %}
96 |
--------------------------------------------------------------------------------
/dashboard/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 | {% block content %}
3 |
29 |
30 |
31 | {{ this_week_stats.closed_request_count }}
32 | requests closed this week.
33 | {{ delta.closed_count}}% change in requests closed from last week to
34 | this.
35 |
36 |
37 |
38 | {{ this_week_stats.request_count}} requests opened this week.
39 | {{ delta.opened_count }}% change in requests from last week to
40 | this.
41 |
42 |
43 |
44 | {{ this_week_stats.average_response }} day{{ this_week_stats.average_response|pluralize }}
45 | average response time.
46 | {{ delta.time }}% change in response time from last week to this.
47 |
48 |
49 |
50 |
51 |
52 | Hello
53 |
54 | San Francisco
55 |
56 | {% for neighborhood in neighborhoods %}
57 | {{ neighborhood.name }}
58 | {% endfor %}
59 |
60 | , let's see how responsive your city is this week.
61 |
62 |
63 | Select a map:
64 |
65 | Street/Sidewalk Cleaning
66 | Trash Cans
67 | Illegal Dumping
68 | Graffiti
69 | Overflowing City Recepticles
70 | Pavement Defect
71 | Sewer
72 | Abandoned Vehicle
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
85 | {% endblock %}
86 |
87 | {% block custom_scripts %}
88 |
89 |
90 |
91 |
92 |
93 |
102 |
103 |
153 | {% endblock %}
154 |
--------------------------------------------------------------------------------
/dashboard/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base/main.html" %}
2 | {% load url from future %}
3 |
4 | {% block content %}
5 |
6 |
Login
7 | {% if form.errors %}
8 |
Your username and password didn't match. Please try again.
9 | {% endif %}
10 |
11 |
27 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/dashboard/templates/map.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
3 | {% block content %}
4 |
9 |
10 | Select a map:
11 |
12 | Street/Sidewalk Cleaning
13 | Trash Cans
14 | Illegal Dumping
15 | Graffiti
16 | Overflowing City Recepticles
17 | Pavement Defect
18 | Sewer
19 | Abandoned Vehicle
20 |
21 |
22 |
23 |
24 | {% endblock %}
25 |
26 | {% block custom_scripts %}
27 |
28 |
29 |
30 |
31 |
32 |
41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/dashboard/templates/neighborhood_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
Neighborhoods
6 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/dashboard/templates/search.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
3 | {% block custom_scripts %}
4 |
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | {% if error %}
11 | Sorry, your lookup failed.
12 | {% endif %}
13 |
14 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/dashboard/templates/street_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
Streets
6 |
7 | {% for street in top_streets %}
8 |
9 | {{ street.street_name|title}}
10 | ({{ street.count }} open request{{street.count|pluralize}})
11 |
12 | {% endfor %}
13 |
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/dashboard/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | import json
4 | import random
5 |
6 | class IndexTest(TestCase):
7 | """Test the index view and related json"""
8 | fixtures = ['test.json']
9 |
10 | def test_success(self):
11 | """Test that the index works"""
12 | response = self.client.get("/")
13 | self.assertEqual(response.status_code, 200)
14 |
15 | def test_template(self):
16 | """Test that the correct templates are being rendered"""
17 | response = self.client.get("/")
18 | self.assertTemplateUsed(response, 'index.html')
19 | self.assertTemplateUsed(response, 'base/main.html')
20 |
21 | def test_api_success(self):
22 | """Test the JSON API"""
23 | rand = random.randint(1,5)
24 | response = self.client.get("/api/home/%s.json" % rand)
25 |
26 | self.assertEqual(response.status_code, 200)
27 |
28 | def test_api_valid(self):
29 | """Test that the JSON is valid"""
30 | rand = random.randint(1,5)
31 | response = self.client.get("/api/home/%s.json" % rand)
32 | data = json.loads(response.content)
33 |
34 | self.assertIsInstance(data, dict)
35 |
36 | class NeighborhoodTest(TestCase):
37 | """Test the neighborhood views"""
38 | fixtures = ['test.json']
39 |
40 | def test_success_list(self):
41 | """Check to make sure the neighborhood list is working"""
42 | response = self.client.get("/neighborhood/")
43 | self.assertEqual(response.status_code, 200)
44 |
45 | def test_success_detail(self):
46 | """Check to make sure neighborhood detail is working"""
47 | rand = random.randint(1, 5)
48 | response = self.client.get("/neighborhood/%s/" % rand)
49 |
50 | self.assertEqual(response.status_code, 200)
51 |
52 | def test_success_api(self):
53 | """Check to make sure the API works"""
54 | rand = random.randint(1, 5)
55 | response = self.client.get("/neighborhood/%s.json" % rand)
56 | self.assertEqual(response.status_code, 200)
57 |
58 | def test_template_list(self):
59 | """Check the template that is rendered for the neighborhood list."""
60 | response = self.client.get("/neighborhood/")
61 | self.assertTemplateUsed(response, "neighborhood_list.html")
62 | self.assertTemplateUsed(response, "base/main.html")
63 |
64 | def test_template_detail(self):
65 | """Check the template that is rendered for the neighborhood detail."""
66 | rand = random.randint(1, 5)
67 | response = self.client.get("/neighborhood/%s/" % rand)
68 |
69 | self.assertTemplateUsed(response, "geo_detail.html")
70 | self.assertTemplateUsed(response, "base/main.html")
71 |
72 | def test_redirect_list(self):
73 | """Check the neighborhood list redirect"""
74 | response = self.client.get("/neighborhood")
75 | self.assertEqual(response.status_code, 301)
76 |
77 | def test_redirect_detail(self):
78 | """Check the neighborhood detail redirect"""
79 | rand = random.randint(1, 5)
80 | response = self.client.get("/neighborhood/%s" % rand)
81 | self.assertEqual(response.status_code, 301)
82 |
83 | def test_valid_api(self):
84 | """Make sure the neighborhood detail api is working"""
85 | rand = random.randint(1, 5)
86 | response = self.client.get("/neighborhood/%s.json" % rand)
87 | data = json.loads(response.content)
88 |
89 | self.assertIsInstance(data, list)
90 |
91 | class StreetTest(TestCase):
92 | """Test the street pages"""
93 | fixtures = ['test.json']
94 | def test_success_list(self):
95 | """Check that the street list works"""
96 | response = self.client.get("/street/")
97 | self.assertEqual(response.status_code, 200)
98 |
99 | def test_success_detail(self):
100 | """Check that the street detail is working"""
101 | rand = random.randint(2, 50)
102 | response = self.client.get("/street/%s/" % rand)
103 | self.assertEqual(response.status_code, 200)
104 |
105 | def test_success_api(self):
106 | """Check that the street api is working"""
107 | rand = random.randint(2, 50)
108 | response = self.client.get("/street/%s.json" % rand)
109 | self.assertEqual(response.status_code, 200)
110 |
111 | def test_template_list(self):
112 | """Check the street list templates"""
113 | response = self.client.get("/street/")
114 | self.assertTemplateUsed(response, "street_list.html")
115 | self.assertTemplateUsed(response, "base/main.html")
116 |
117 | def test_template_detail(self):
118 | """Check the street detail templates"""
119 | rand = random.randint(2, 50)
120 | response = self.client.get("/street/%s/" % rand)
121 | self.assertTemplateUsed(response, "geo_detail.html")
122 | self.assertTemplateUsed(response, "base/main.html")
123 |
124 | def test_redirect_list(self):
125 | """Check street list redirect"""
126 | response = self.client.get("/street")
127 | self.assertEqual(response.status_code, 301)
128 |
129 | def test_redirect_detail(self):
130 | """Check street detail redirect"""
131 | rand = random.randint(2, 50)
132 | response = self.client.get("/street/%s" % rand)
133 | self.assertEqual(response.status_code, 301)
134 |
135 | def test_valid_api(self):
136 | """Check that the API is valid"""
137 | rand = random.randint(2, 50)
138 | response = self.client.get("/street/%s.json" % rand)
139 | data = json.loads(response.content)
140 | self.assertIsInstance(data, list)
141 |
142 | class SearchTest(TestCase):
143 | """Test the search"""
144 |
145 | def test_success_search(self):
146 | """Check for success rendering the status page"""
147 | response = self.client.get("/search/")
148 | self.assertEqual(response.status_code, 200)
149 |
150 | def test_template_search(self):
151 | """Check the template rendered on the search page"""
152 | response = self.client.get("/search/")
153 | self.assertTemplateUsed(response, "search.html")
154 | self.assertTemplateUsed(response, "base/main.html")
155 |
156 | def test_redirect_search(self):
157 | """Check the redirect on the search page"""
158 | response = self.client.get("/search")
159 | self.assertEqual(response.status_code, 301)
160 |
161 | class MapTest(TestCase):
162 | def test_success_map(self):
163 | """Check that the map page works"""
164 | response = self.client.get("/map/")
165 | self.assertEqual(response.status_code, 200)
166 |
167 | def test_template_map(self):
168 | """Check the templates rendered on the map page"""
169 | response = self.client.get("/map/")
170 | self.assertTemplateUsed(response, "map.html")
171 | self.assertTemplateUsed(response, "base/main.html")
172 |
173 | def test_redirect_map(self):
174 | """Check that the redirect works on the map page"""
175 | response = self.client.get("/map")
176 | self.assertEqual(response.status_code, 301)
177 |
--------------------------------------------------------------------------------
/dashboard/unit_tests.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import unittest
3 | from dateutil import parser
4 | from unittest import TestCase, main
5 | from management.commands.utilities import *
6 |
7 | class _TestUpdateDb(unittest.TestCase):
8 |
9 | def test_validate_dt_value(self):
10 | # test that a "proper" datetime does not throw exception
11 | test_time = datetime.datetime(2012, 3, 14, 0, 0, 0)
12 | result = validate_dt_value(test_time)
13 | self.assertEqual(None, result)
14 |
15 | # test that ValueError is raised if microseconds is non-zero
16 | with self.assertRaises(ValueError) as context_manager:
17 | test_time = datetime.datetime(2012, 3, 14, 0, 0, 0, 100)
18 | validate_dt_value(test_time)
19 |
20 | ex = context_manager.exception
21 | self.assertEqual('Microseconds on datetime must ' \
22 | 'be 0: 2012-03-14 00:00:00.000100', ex.message)
23 |
24 | # test that ValueError is raised if tzinfo is not None
25 | with self.assertRaises(ValueError) as context_manager:
26 | test_time = parser.parse("2012-02-21T10:57:47-05:00")
27 | validate_dt_value(test_time)
28 |
29 | ex = context_manager.exception
30 | self.assertEqual('Tzinfo on datetime must be None: ' \
31 | '2012-02-21 10:57:47-05:00', ex.message)
32 |
33 | def test_transform_date(self):
34 | # transform an ISO 8601 string that represents 2/21/2012 at 10:57:47
35 | # with a 5 hour offset
36 | d = transform_date("2012-02-21T10:57:47-05:00")
37 | # expect to recieve a string formatted with just YYYY-MM-DD HH:MM
38 | self.assertEqual("2012-02-21 10:57", d)
39 |
40 | def test_get_time_range(self):
41 | # if we create a start date of 3/14
42 | start_date = datetime.datetime(2012, 3, 14, 0, 0, 0)
43 | # our expected start and end dates are as follows
44 | expected_start_date = start_date - datetime.timedelta(days=1)
45 | expected_end_date = datetime.datetime(2012, 3, 14, 0, 0, 0)
46 | # call the method and assert we get what we expect
47 | start, end = get_time_range(start_date)
48 | self.assertEqual(end, expected_end_date)
49 | self.assertEqual(start, expected_start_date)
50 |
51 | # same as above but make sure that a start_date passed in
52 | # as NOT midnight gets set to midnight
53 | start_date = datetime.datetime(2012, 3, 14, 1, 30, 30)
54 | # our expected start and end dates are as follows
55 | expected_start_date = start_date.replace(hour=0, minute=0, second=0,
56 | microsecond=0) - datetime.timedelta(days=1)
57 | expected_end_date = datetime.datetime(2012, 3, 14, 0, 0, 0)
58 | # call the method and assert we get what we expect
59 | start, end = get_time_range(start_date)
60 | self.assertEqual(end, expected_end_date)
61 | self.assertEqual(start, expected_start_date)
62 |
63 | # same as above but pass None and check that default
64 | # behavior works as intended
65 | # our expected start and end dates are as follows
66 | expected_end_date = datetime.datetime.utcnow().replace(hour=0, minute=0,
67 | second=0, microsecond=0) - datetime.timedelta(days=1)
68 | expected_start_date = expected_end_date - datetime.timedelta(days=1)
69 | # call the method and assert we get what we expect
70 | start, end = get_time_range()
71 | self.assertEqual(end, expected_end_date)
72 | self.assertEqual(start, expected_start_date)
73 |
74 | if __name__ == '__main__':
75 | suite = unittest.TestLoader().loadTestsFromTestCase(_TestUpdateDb)
76 | unittest.TextTestRunner(verbosity=2).run(suite)
77 |
--------------------------------------------------------------------------------
/dashboard/utils.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import qsstats
4 | from io import StringIO
5 | from django.db.models import Model
6 | from django.db.models.query import QuerySet
7 | from django.utils.encoding import smart_unicode
8 | from django.utils.simplejson import dumps
9 | from django.contrib.gis.db.models.fields import GeometryField
10 | from django.utils import simplejson
11 | from django.http import HttpResponse
12 | from django.db.models import Count
13 |
14 | def run_stats(request_obj, **kwargs):
15 | """
16 |
17 | Returns stats on a given request set.
18 |
19 |
20 | """
21 | stats = {}
22 |
23 |
24 | try:
25 | # Average response time.
26 | stats['average_response'] = request_obj.filter(status="Closed") \
27 | .extra({"average": "avg(updated_datetime - requested_datetime)"}) \
28 | .values("average")
29 | stats['average_response'] = stats['average_response'][0]["average"].days
30 |
31 | # Total request count.
32 | stats['request_count'] = request_obj.count()
33 |
34 | # Request types.
35 | if kwargs.has_key('request_types') is False:
36 | stats['request_types'] = request_obj.values('service_name') \
37 | .annotate(count=Count('service_name')).order_by('-count')[:10]
38 |
39 | # Opened requests by day (limit: 30)
40 | time_delta = datetime.timedelta(days=30)
41 | latest = request_obj.latest('requested_datetime')
42 | qss = qsstats.QuerySetStats(request_obj, 'requested_datetime')
43 | time_series = qss.time_series(latest.requested_datetime - time_delta,
44 | latest.requested_datetime)
45 | stats['opened_by_day'] = [t[1] for t in time_series]
46 |
47 | # Open request count.
48 | stats['open_request_count'] = request_obj.filter(status="Open").count()
49 |
50 | # Closed request count.
51 | stats['closed_request_count'] = request_obj.filter(status="Closed").count()
52 |
53 | # Recently opened requests.
54 | if kwargs.has_key('open_requests') is False:
55 | stats['open_requests'] = request_obj.filter(status="Open") \
56 | .order_by('-requested_datetime')[:10]
57 |
58 | except:
59 | stats['average_response'] = 0
60 | stats['request_count'] = 0
61 | stats['request_types'] = []
62 | stats['open_request_count'] = 0
63 | stats['closed_request_count'] = 0
64 | stats['opened_by_day'] = [0]
65 |
66 | # Return
67 | return stats
68 |
69 | def calculate_delta(new, old):
70 | try:
71 | delta = int(round(((float(new) / old)-1) * 100))
72 | except:
73 | delta = 100
74 |
75 | return delta
76 |
77 | # Handle string/date conversion.
78 | def str_to_day(date):
79 | """Convert a YYYY-MM-DD string to a datetime object"""
80 | return datetime.datetime.strptime(date, '%Y-%m-%d')
81 |
82 | def day_to_str(date):
83 | """Convert a datetime object into a YYYY-MM-DD string"""
84 | return datetime.datetime.strftime(date, '%Y-%m-%d')
85 |
86 | def date_range(begin, end=None):
87 | """Returns a tuple of datetimes spanning the given range"""
88 | if end == None:
89 | date = str_to_day(begin)
90 | begin = datetime.datetime.combine(date, datetime.time.min)
91 | end = datetime.datetime.combine(date, datetime.time.max)
92 | else:
93 | begin = str_to_day(begin)
94 | end = str_to_day(end)
95 |
96 | return (begin, end)
97 |
98 | def dt_handler(obj):
99 | if isinstance(obj, datetime.datetime):
100 | return obj.isoformat()
101 | else:
102 | return None
103 |
104 | ##
105 | # Taken from http://geodjango-basic-apps.googlecode.com/svn/trunk/projects/alpha_shapes/clustr/shortcuts.py
106 | ##
107 | def render_to_geojson(query_set, geom_field=None, mimetype='text/plain', pretty_print=True, exclude=[]):
108 | '''
109 |
110 | Shortcut to render a GeoJson FeatureCollection from a Django QuerySet.
111 | Currently computes a bbox and adds a crs member as a sr.org link
112 |
113 | '''
114 | collection = {}
115 |
116 | # Find the geometry field
117 | # qs.query._geo_field()
118 |
119 | fields = query_set.model._meta.fields
120 | geo_fields = [f for f in fields if isinstance(f, GeometryField)]
121 |
122 | #attempt to assign geom_field that was passed in
123 | if geom_field:
124 | geo_fieldnames = [x.name for x in geo_fields]
125 | try:
126 | geo_field = geo_fields[geo_fieldnames.index(geom_field)]
127 | except:
128 | raise Exception('%s is not a valid geometry on this model' % geom_field)
129 | else:
130 | geo_field = geo_fields[0] # no support yet for multiple geometry fields
131 |
132 | #remove other geom fields from showing up in attributes
133 | if len(geo_fields) > 1:
134 | for gf in geo_fields:
135 | if gf.name not in exclude: exclude.append(gf.name)
136 | exclude.remove(geo_field.name)
137 |
138 | # Gather the projection information
139 | crs = {}
140 | crs['type'] = "link"
141 | crs_properties = {}
142 | crs_properties['href'] = 'http://spatialreference.org/ref/epsg/%s/' % geo_field.srid
143 | crs_properties['type'] = 'proj4'
144 | crs['properties'] = crs_properties
145 | collection['crs'] = crs
146 |
147 | # Build list of features
148 | features = []
149 | if query_set:
150 | for item in query_set:
151 | feat = {}
152 | feat['type'] = 'Feature'
153 | d= item.__dict__.copy()
154 | g = getattr(item,geo_field.name)
155 | d.pop(geo_field.name)
156 | for field in exclude:
157 | d.pop(field)
158 | feat['geometry'] = simplejson.loads(g.geojson)
159 | feat['properties'] = d
160 | features.append(feat)
161 | else:
162 | pass #features.append({'type':'Feature','geometry': {},'properties':{}})
163 |
164 | # Label as FeatureCollection and add Features
165 | collection['type'] = "FeatureCollection"
166 | collection['features'] = features
167 |
168 | # Attach extent of all features
169 | #if query_set:
170 | # #collection['bbox'] = [x for x in query_set.extent()]
171 | # agg = query_set.unionagg()
172 | # collection['bbox'] = [agg.extent]
173 | # collection['centroid'] = [agg.point_on_surface.x,agg.point_on_surface.y]
174 |
175 | # Return response
176 | response = HttpResponse()
177 | if pretty_print:
178 | response.write('%s' % simplejson.dumps(collection, indent=1))
179 | else:
180 | response.write('%s' % simplejson.dumps(collection))
181 | response['Content-length'] = str(len(response.content))
182 | response['Content-Type'] = mimetype
183 | return response
184 |
185 | ##
186 | # JSON SERIALIZER FROM:
187 | ##
188 | class UnableToSerializeError(Exception):
189 | """ Error for not implemented classes """
190 | def __init__(self, value):
191 | self.value = value
192 | Exception.__init__(self)
193 |
194 | def __str__(self):
195 | return repr(self.value)
196 |
197 | class JSONSerializer():
198 | boolean_fields = ['BooleanField', 'NullBooleanField']
199 | datetime_fields = ['DatetimeField', 'DateField', 'TimeField']
200 | number_fields = ['IntegerField', 'AutoField', 'DecimalField', 'FloatField', 'PositiveSmallIntegerField']
201 |
202 | def serialize(self, obj, **options):
203 | self.options = options
204 |
205 | self.stream = options.pop("stream", StringIO())
206 | self.selectedFields = options.pop("fields", None)
207 | self.ignoredFields = options.pop("ignored", None)
208 | self.use_natural_keys = options.pop("use_natural_keys", False)
209 | self.currentLoc = ''
210 |
211 | self.level = 0
212 |
213 | self.start_serialization()
214 |
215 | self.handle_object(obj)
216 |
217 | self.end_serialization()
218 | return self.getvalue()
219 |
220 | def get_string_value(self, obj, field):
221 | """Convert a field's value to a string."""
222 | return smart_unicode(field.value_to_string(obj))
223 |
224 | def start_serialization(self):
225 | """Called when serializing of the queryset starts."""
226 | pass
227 |
228 | def end_serialization(self):
229 | """Called when serializing of the queryset ends."""
230 | pass
231 |
232 | def start_array(self):
233 | """Called when serializing of an array starts."""
234 | self.stream.write(u'[')
235 | def end_array(self):
236 | """Called when serializing of an array ends."""
237 | self.stream.write(u']')
238 |
239 | def start_object(self):
240 | """Called when serializing of an object starts."""
241 | self.stream.write(u'{')
242 |
243 | def end_object(self):
244 | """Called when serializing of an object ends."""
245 | self.stream.write(u'}')
246 |
247 | def handle_object(self, object):
248 | """ Called to handle everything, looks for the correct handling """
249 | if isinstance(object, dict):
250 | self.handle_dictionary(object)
251 | elif isinstance(object, list):
252 | self.handle_list(object)
253 | elif isinstance(object, Model):
254 | self.handle_model(object)
255 | elif isinstance(object, QuerySet):
256 | self.handle_queryset(object)
257 | elif isinstance(object, bool):
258 | self.handle_simple(object)
259 | elif isinstance(object, int) or isinstance(object, float) or isinstance(object, long):
260 | self.handle_simple(object)
261 | elif isinstance(object, basestring):
262 | self.handle_simple(object)
263 | else:
264 | raise UnableToSerializeError(type(object))
265 |
266 | def handle_dictionary(self, d):
267 | """Called to handle a Dictionary"""
268 | i = 0
269 | self.start_object()
270 | for key, value in d.iteritems():
271 | self.currentLoc += key+'.'
272 | #self.stream.write(unicode(self.currentLoc))
273 | i += 1
274 | self.handle_simple(key)
275 | self.stream.write(u': ')
276 | self.handle_object(value)
277 | if i != len(d):
278 | self.stream.write(u', ')
279 | self.currentLoc = self.currentLoc[0:(len(self.currentLoc)-len(key)-1)]
280 | self.end_object()
281 |
282 | def handle_list(self, l):
283 | """Called to handle a list"""
284 | self.start_array()
285 |
286 | for value in l:
287 | self.handle_object(value)
288 | if l.index(value) != len(l) -1:
289 | self.stream.write(u', ')
290 |
291 | self.end_array()
292 |
293 | def handle_model(self, mod):
294 | """Called to handle a django Model"""
295 | self.start_object()
296 |
297 | for field in mod._meta.local_fields:
298 | if field.rel is None:
299 | if self.selectedFields is None or field.attname in self.selectedFields or field.attname:
300 | if self.ignoredFields is None or self.currentLoc + field.attname not in self.ignoredFields:
301 | self.handle_field(mod, field)
302 | else:
303 | if self.selectedFields is None or field.attname[:-3] in self.selectedFields:
304 | if self.ignoredFields is None or self.currentLoc + field.attname[:-3] not in self.ignoredFields:
305 | self.handle_fk_field(mod, field)
306 | for field in mod._meta.many_to_many:
307 | if self.selectedFields is None or field.attname in self.selectedFields:
308 | if self.ignoredFields is None or self.currentLoc + field.attname not in self.ignoredFields:
309 | self.handle_m2m_field(mod, field)
310 | self.stream.seek(self.stream.tell()-2)
311 | self.end_object()
312 |
313 | def handle_queryset(self, queryset):
314 | """Called to handle a django queryset"""
315 | self.start_array()
316 | it = 0
317 | for mod in queryset:
318 | it += 1
319 | self.handle_model(mod)
320 | if queryset.count() != it:
321 | self.stream.write(u', ')
322 | self.end_array()
323 |
324 | def handle_field(self, mod, field):
325 | """Called to handle each individual (non-relational) field on an object."""
326 | self.handle_simple(field.name)
327 | if field.get_internal_type() in self.boolean_fields:
328 | if field.value_to_string(mod) == 'True':
329 | self.stream.write(u': true')
330 | elif field.value_to_string(mod) == 'False':
331 | self.stream.write(u': false')
332 | else:
333 | self.stream.write(u': undefined')
334 | else:
335 | self.stream.write(u': ')
336 | self.handle_simple(field.value_to_string(mod))
337 | self.stream.write(u', ')
338 |
339 | def handle_fk_field(self, mod, field):
340 | """Called to handle a ForeignKey field."""
341 | related = getattr(mod, field.name)
342 | if related is not None:
343 | if field.rel.field_name == related._meta.pk.name:
344 | # Related to remote object via primary key
345 | pk = related._get_pk_val()
346 | else:
347 | # Related to remote object via other field
348 | pk = getattr(related, field.rel.field_name)
349 | d = {
350 | 'pk': pk,
351 | }
352 | if self.use_natural_keys and hasattr(related, 'natural_key'):
353 | d.update({'natural_key': related.natural_key()})
354 | if type(d['pk']) == str and d['pk'].isdigit():
355 | d.update({'pk': int(d['pk'])})
356 |
357 | self.handle_simple(field.name)
358 | self.stream.write(u': ')
359 | self.handle_object(d)
360 | self.stream.write(u', ')
361 |
362 | def handle_m2m_field(self, mod, field):
363 | """Called to handle a ManyToManyField."""
364 | if field.rel.through._meta.auto_created:
365 | self.handle_simple(field.name)
366 | self.stream.write(u': ')
367 | self.start_array()
368 | hasRelationships = False
369 | for relobj in getattr(mod, field.name).iterator():
370 | hasRelationships = True
371 | pk = relobj._get_pk_val()
372 | d = {
373 | 'pk': pk,
374 | }
375 | if self.use_natural_keys and hasattr(relobj, 'natural_key'):
376 | d.update({'natural_key': relobj.natural_key()})
377 | if type(d['pk']) == str and d['pk'].isdigit():
378 | d.update({'pk': int(d['pk'])})
379 |
380 | self.handle_simple(d)
381 | self.stream.write(u', ')
382 | if hasRelationships:
383 | self.stream.seek(self.stream.tell()-2)
384 | self.end_array()
385 | self.stream.write(u', ')
386 |
387 | def handle_simple(self, simple):
388 | """ Called to handle values that can be handled via simplejson """
389 | self.stream.write(unicode(dumps(simple)))
390 |
391 | def getvalue(self):
392 | """Return the fully serialized object (or None if the output stream is not seekable).sss """
393 | if callable(getattr(self.stream, 'getvalue', None)):
394 | return self.stream.getvalue()
395 |
396 | def json_response_from(response):
397 | jsonSerializer = JSONSerializer()
398 | return HttpResponse(jsonSerializer.serialize(response, use_natural_keys=True), mimetype='application/json')
399 |
--------------------------------------------------------------------------------
/dashboard/views.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import qsstats
3 | import time
4 | import json
5 | import urllib
6 | import urllib2
7 |
8 | from django.template import Context
9 | from django.shortcuts import render, redirect
10 | from django.db.models import Count
11 | from django.http import HttpResponse
12 | from django.contrib.auth.decorators import login_required
13 |
14 | from django.contrib.gis.geos import Point
15 | from django.contrib.gis.measure import Distance as D
16 |
17 | from dashboard.models import Request, City, Geography, Street
18 | from dashboard.decorators import ApiHandler
19 | from dashboard.utils import str_to_day, day_to_str, \
20 | date_range, dt_handler, render_to_geojson, run_stats, calculate_delta, \
21 | json_response_from
22 |
23 |
24 | def index(request, geography=None, is_json=False):
25 | """
26 | Homepage view. Can also return json for the city or neighborhoods.
27 | """
28 | if geography is None:
29 | requests = Request.objects.all()
30 | else:
31 | neighborhood = Geography.objects.get(pk=geography)
32 | requests = Request.objects.filter(geo_point__contained=neighborhood.geo)
33 |
34 | total_open = requests.filter(status="Open").count()
35 | most_recent = requests.latest('requested_datetime')
36 | minus_7 = most_recent.requested_datetime-datetime.timedelta(days=7)
37 | minus_14 = most_recent.requested_datetime-datetime.timedelta(days=14)
38 |
39 | this_week = requests.filter(requested_datetime__range= \
40 | (minus_7, most_recent.requested_datetime))
41 | last_week = requests.filter(requested_datetime__range= \
42 | (minus_14, minus_7))
43 |
44 | this_week_stats = run_stats(this_week, request_types=False,
45 | open_requests=False)
46 | last_week_stats = run_stats(last_week, request_types=False,
47 | open_requests=False)
48 |
49 | # Calculate deltas
50 | delta = {}
51 | delta['count'] = calculate_delta(this_week_stats['request_count'],
52 | last_week_stats['request_count'])
53 | delta['closed_count'] = calculate_delta( \
54 | this_week_stats['closed_request_count'],
55 | last_week_stats['closed_request_count'])
56 | delta['opened_count'] = calculate_delta( \
57 | this_week_stats['open_request_count'],
58 | last_week_stats['open_request_count'])
59 | delta['time'] = calculate_delta(this_week_stats['average_response'],
60 | last_week_stats['average_response'])
61 |
62 | # Put everything in a dict so we can do what we want with it.
63 | c_dict = {
64 | 'open_tickets': total_open,
65 | 'this_week_stats': this_week_stats,
66 | 'last_week_stats': last_week_stats,
67 | 'delta': delta,
68 | }
69 |
70 | if is_json is False:
71 | neighborhoods = Geography.objects.all()
72 | c_dict['neighborhoods'] = neighborhoods
73 | c_dict['latest'] = most_recent.requested_datetime
74 | c = Context(c_dict)
75 | return render(request, 'index.html', c)
76 | else:
77 | data = json.dumps(c_dict, True)
78 | return HttpResponse(data, content_type='application/json')
79 |
80 |
81 | # Neighborhood specific pages.
82 | def neighborhood_list(request):
83 | """
84 | List the neighborhoods.
85 | """
86 | neighborhoods = Geography.objects.all()
87 |
88 | c = Context({
89 | 'neighborhoods': neighborhoods
90 | })
91 |
92 | return render(request, 'neighborhood_list.html', c)
93 |
94 |
95 | def neighborhood_detail(request, neighborhood_id):
96 | """
97 |
98 | Show detail for a specific neighborhood. Uses templates/geo_detail.html
99 |
100 | """
101 | neighborhood = Geography.objects.get(pk=neighborhood_id)
102 | nearby = Geography.objects.all().distance(neighborhood.geo) \
103 | .exclude(name=neighborhood.name).order_by('distance')[:5]
104 |
105 | # Get the requests inside the neighborhood, run the stats
106 | requests = Request.objects.filter(geo_point__contained=neighborhood.geo)
107 | stats = run_stats(requests)
108 |
109 | title = neighborhood.name
110 |
111 | neighborhood.geo.transform(4326)
112 | simple_shape = neighborhood.geo.simplify(.0003,
113 | preserve_topology=True)
114 |
115 | c = Context({
116 | 'title': title,
117 | 'geometry': simple_shape.geojson,
118 | 'centroid': [simple_shape.centroid[0], simple_shape.centroid[1]],
119 | 'extent': simple_shape.extent,
120 | 'stats': stats,
121 | 'nearby': nearby,
122 | 'type': 'neighborhood',
123 | 'id': neighborhood_id
124 | })
125 |
126 | return render(request, 'geo_detail.html', c)
127 |
128 |
129 | def neighborhood_detail_json(request, neighborhood_id):
130 | """
131 |
132 | Download JSON of the requests that built the page. Caution: slow!
133 |
134 | TODO: Speed it up.
135 |
136 | """
137 | neighborhood = Geography.objects.get(pk=neighborhood_id)
138 | requests = Request.objects.filter(geo_point__contained=neighborhood.geo)
139 | return json_response_from(requests)
140 |
141 |
142 | # Street specific pages.
143 | def street_list(request):
144 | """
145 |
146 | List the top 10 streets by open service requests.
147 |
148 | """
149 | streets = Street.objects.filter(request__status="Open") \
150 | .annotate(count=Count('request__service_request_id')) \
151 | .order_by('-count')[:10]
152 |
153 | c = Context({
154 | 'top_streets': streets
155 | })
156 |
157 | return render(request, 'street_list.html', c)
158 |
159 |
160 | def street_view(request, street_id):
161 | """
162 | View details for a specific street. Renders geo_detail.html like
163 | neighborhood_detail does.
164 | """
165 | street = Street.objects.get(pk=street_id)
166 | nearby = Street.objects.all().distance(street.line) \
167 | .exclude(street_name=street.street_name).order_by('distance')[:5]
168 | neighborhood = Geography.objects.all() \
169 | .distance(street.line).order_by('distance')[:1]
170 |
171 | # Max/min addresses
172 | addresses = [street.left_low_address, street.left_high_address,
173 | street.right_low_address, street.right_high_address]
174 | addresses.sort()
175 |
176 | title = "%s %i - %i" % (street.street_name, addresses[0], addresses[3])
177 |
178 | # Requests
179 | requests = Request.objects.filter(street=street_id)
180 | stats = run_stats(requests)
181 |
182 | street.line.transform(4326)
183 |
184 | c = Context({
185 | 'title': title,
186 | 'geometry': street.line.geojson,
187 | 'centroid': [street.line.centroid[0], street.line.centroid[1]],
188 | 'extent': street.line.extent,
189 | 'stats': stats,
190 | 'nearby': nearby,
191 | 'neighborhood': neighborhood[0],
192 | 'type': 'street',
193 | 'id': street_id
194 | })
195 |
196 | return render(request, 'geo_detail.html', c)
197 |
198 |
199 | def street_view_json(request, street_id):
200 | """
201 |
202 | Download the JSON for the requests that built the page.
203 |
204 | """
205 | requests = Request.objects.filter(street=street_id)
206 | return json_response_from(requests)
207 |
208 |
209 | # Search for an address!
210 | def street_search(request):
211 | """
212 | Do a San Francisco specific geocode and then match that against our street
213 | centerline data.
214 | """
215 | query = request.GET.get('q')
216 | lat = request.GET.get('lat')
217 | lon = request.GET.get('lng')
218 | if not query:
219 | # They haven't searched for anything.
220 | return render(request, 'search.html')
221 | elif query and not lat:
222 | # Lookup the search string with Yahoo!
223 | url = "http://where.yahooapis.com/geocode"
224 | params = {"addr": query,
225 | "line2": "San Francisco, CA",
226 | "flags": "J",
227 | "appid": "1I9Jh.3V34HMiBXzxZRYmx.DO1JfVJtKh7uvDTJ4R0dRXnMnswRHXbai1NFdTzvC" }
228 |
229 | query_params = urllib.urlencode(params)
230 | data = urllib2.urlopen("%s?%s" % (url, query_params)).read()
231 |
232 | print data
233 |
234 | temp_json = json.loads(data)
235 |
236 | if temp_json['ResultSet']['Results'][0]['quality'] > 50:
237 | lon = temp_json['ResultSet']['Results'][0]["longitude"]
238 | lat = temp_json['ResultSet']['Results'][0]["latitude"]
239 | else:
240 | lat, lon = None, None
241 |
242 | if lat and lon:
243 | point = Point(float(lon), float(lat))
244 | point.srid = 4326
245 | point.transform(900913)
246 | nearest_street = Street.objects \
247 | .filter(line__dwithin=(point, D(m=100))) \
248 | .distance(point).order_by('distance')[:1]
249 | try:
250 | return redirect(nearest_street[0])
251 | except IndexError:
252 | pass
253 | c = Context({'error': True})
254 | return render(request, 'search.html', c)
255 |
256 |
257 | def map(request):
258 | """
259 | Simply render the map.
260 |
261 | TODO: Get the centroid and bounding box of the city and set that. (See
262 | neighborhood_detail and geo_detail.html for how this would look)
263 | """
264 | return render(request, 'map.html')
265 |
266 | # Admin Pages
267 | @login_required
268 | def admin(request):
269 | """
270 |
271 | Admin home page. Just list the cities.
272 |
273 | """
274 | cities = City.objects.all()
275 | c = Context({'cities': cities})
276 | return render(request, 'admin/index.html', c)
277 |
278 | @login_required
279 | def city_admin(request, shortname=None):
280 | """
281 |
282 | Administer a specific city (and associated data)
283 |
284 | """
285 | city = City.objects.get(short_name=shortname)
286 | geographies = Geography.objects.filter(city=city.id).count()
287 | streets = Street.objects.filter(city=city.id).count()
288 | requests = Request.objects.filter(city=city.id).count()
289 |
290 | c = Context({
291 | 'city': city,
292 | 'geographies': geographies,
293 | 'streets': streets,
294 | 'requests': requests
295 | })
296 |
297 | return render(request, 'admin/city_view.html', c)
298 |
299 | @login_required
300 | def city_add(request):
301 | """
302 |
303 | Add a new city.
304 |
305 | """
306 | return render(request, 'admin/city_add.html')
307 |
308 | # API Views
309 | @ApiHandler
310 | def ticket_days(request, ticket_status="open", start=None, end=None,
311 | num_days=None):
312 | '''Returns JSON with the number of opened/closed tickets in a specified
313 | date range'''
314 |
315 | # If no start or end variables are passed, do the past 30 days. If one is
316 | # passed, check if num_days and do the past num_days. If num_days isn't
317 | # passed, just do one day. Else, do the range.
318 | if start is None and end is None:
319 | num_days = int(num_days) if num_days is not None else 29
320 |
321 | end = datetime.date.today()
322 | start = end - datetime.timedelta(days=num_days)
323 | elif end is not None and num_days is not None:
324 | num_days = int(num_days) - 1
325 | end = str_to_day(end)
326 | start = end - datetime.timedelta(days=num_days)
327 | elif end is not None and start is None:
328 | end = str_to_day(end)
329 | start = end
330 | else:
331 | start = str_to_day(start)
332 | end = str_to_day(end)
333 |
334 | if ticket_status == "open":
335 | request = Request.objects.filter(status="Open") \
336 | .filter(requested_datetime__range=date_range(day_to_str(start),
337 | day_to_str(end)))
338 | stats = qsstats.QuerySetStats(request, 'requested_datetime')
339 | elif ticket_status == "closed":
340 | request = Request.objects.filter(status="Closed")
341 | stats = qsstats.QuerySetStats(request, 'updated_datetime') \
342 | .filter(requested_datetime__range=date_range(day_to_str(start),
343 | day_to_str(end)))
344 | elif ticket_status == "both":
345 | request_opened = Request.objects.filter(status="Open") \
346 | .filter(requested_datetime__range=date_range(day_to_str(start),
347 | day_to_str(end)))
348 | stats_opened = qsstats.QuerySetStats(request_opened,
349 | 'requested_datetime')
350 |
351 | request_closed = Request.objects.filter(status="Closed") \
352 | .filter(requested_datetime__range=date_range(day_to_str(start),
353 | day_to_str(end)))
354 | stats_closed = qsstats.QuerySetStats(request_closed,
355 | 'updated_datetime')
356 |
357 | data = []
358 |
359 | try:
360 | raw_data = stats.time_series(start, end)
361 |
362 | for row in raw_data:
363 | temp_data = {'date': int(time.mktime(row[0].timetuple())), 'count': row[1]}
364 | data.append(temp_data)
365 | except:
366 | opened_data = stats_opened.time_series(start, end)
367 | closed_data = stats_closed.time_series(start, end)
368 | for i in range(len(opened_data)):
369 | temp_data = {
370 | 'date': int(time.mktime(opened_data[i][0].timetuple())),
371 | 'open_count': opened_data[i][1],
372 | 'closed_count': closed_data[i][1],
373 | }
374 | data.append(temp_data)
375 | return data
376 |
377 | @ApiHandler
378 | def ticket_day(request, begin=day_to_str(datetime.date.today()), end=None):
379 | """
380 |
381 | Get service_name stats for a range of dates.
382 |
383 | """
384 | if end == None:
385 | key = begin
386 | else:
387 | key = "%s - %s" % (begin, end)
388 |
389 | # Request and group by service_name.
390 | requests = Request.objects \
391 | .filter(requested_datetime__range=date_range(begin, end)) \
392 | .values('service_name').annotate(count=Count('service_name')) \
393 | .order_by('-count')
394 |
395 | data = {key: [item for item in requests]}
396 | return data
397 |
398 | # List requests in a given date range
399 | @ApiHandler
400 | def list_requests(request, begin=day_to_str(datetime.date.today()), end=None):
401 | """
402 |
403 | List requests opened in a given date range
404 |
405 | """
406 | requests = Request.objects \
407 | .filter(requested_datetime__range=date_range(begin,end))
408 |
409 | data = [item for item in requests.values()]
410 | return data
411 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | import imp
4 | try:
5 | imp.find_module('settings') # Assumed to be in the same directory.
6 | except ImportError:
7 | import sys
8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
9 | sys.exit(1)
10 |
11 | import settings
12 |
13 | if __name__ == "__main__":
14 | execute_manager(settings)
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # ----------------------
2 | # Django
3 | # ----------------------
4 | Django==1.3.1
5 | -e git+https://github.com/kmike/django-qsstats-magic.git#egg=django-qsstats-magic
6 |
7 |
8 | # ----------------------
9 | # Database
10 | # ----------------------
11 | psycopg2==2.4.1
12 |
13 |
14 | # ----------------------
15 | # Testing
16 | # ----------------------
17 | mock
18 | coverage
19 | django-test-coverage
20 |
21 |
22 | # ----------------------
23 | # Utilities
24 | # ----------------------
25 | python-dateutil==1.5
26 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | # Set PATH
2 | import os
3 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
4 |
5 | # Django settings for open311dashboard project.
6 |
7 | DEBUG = True
8 | TEMPLATE_DEBUG = DEBUG
9 |
10 | ADMINS = (
11 | # ('Your Name', 'your_email@example.com'),
12 | )
13 |
14 | MANAGERS = ADMINS
15 |
16 | DATABASES = {
17 | 'default': {
18 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
19 | 'NAME': '', # Or path to database file if using sqlite3.
20 | 'USER': '', # Not used with sqlite3.
21 | 'PASSWORD': '', # Not used with sqlite3.
22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
23 | 'PORT': '', # Set to empty string for default. Not used with sqlite3.
24 | }
25 | }
26 |
27 | # Local time zone for this installation. Choices can be found here:
28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 | # although not all choices may be available on all operating systems.
30 | # On Unix systems, a value of None will cause Django to use the same
31 | # timezone as the operating system.
32 | # If running in a Windows environment this must be set to the same as your
33 | # system time zone.
34 | TIME_ZONE = 'America/Chicago'
35 |
36 | # Language code for this installation. All choices can be found here:
37 | # http://www.i18nguy.com/unicode/language-identifiers.html
38 | LANGUAGE_CODE = 'en-us'
39 |
40 | SITE_ID = 1
41 |
42 | # If you set this to False, Django will make some optimizations so as not
43 | # to load the internationalization machinery.
44 | USE_I18N = True
45 |
46 | # If you set this to False, Django will not format dates, numbers and
47 | # calendars according to the current locale
48 | USE_L10N = True
49 |
50 | # Absolute filesystem path to the directory that will hold user-uploaded files.
51 | # Example: "/home/media/media.lawrence.com/media/"
52 | MEDIA_ROOT = ''
53 |
54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 | # trailing slash.
56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 | MEDIA_URL = ''
58 |
59 | # Absolute path to the directory static files should be collected to.
60 | # Don't put anything in this directory yourself; store your static files
61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 | # Example: "/home/media/media.lawrence.com/static/"
63 | STATIC_ROOT = ''
64 |
65 | # URL prefix for static files.
66 | # Example: "http://media.lawrence.com/static/"
67 | STATIC_URL = '/static/'
68 |
69 | # URL prefix for admin static files -- CSS, JavaScript and images.
70 | # Make sure to use a trailing slash.
71 | # Examples: "http://foo.com/static/admin/", "/static/admin/".
72 | ADMIN_MEDIA_PREFIX = '/static/admin/'
73 |
74 | # Additional locations of static files
75 | STATICFILES_DIRS = (
76 | # Put strings here, like "/home/html/static" or "C:/www/django/static".
77 | # Always use forward slashes, even on Windows.
78 | # Don't forget to use absolute paths, not relative paths.
79 | )
80 |
81 | # List of finder classes that know how to find static files in
82 | # various locations.
83 | STATICFILES_FINDERS = (
84 | 'django.contrib.staticfiles.finders.FileSystemFinder',
85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
86 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
87 | )
88 |
89 | # Make this unique, and don't share it with anybody.
90 | SECRET_KEY = 'CHANGEME'
91 |
92 | # List of callables that know how to import templates from various sources.
93 | TEMPLATE_LOADERS = (
94 | 'django.template.loaders.filesystem.Loader',
95 | 'django.template.loaders.app_directories.Loader',
96 | # 'django.template.loaders.eggs.Loader',
97 | )
98 |
99 | MIDDLEWARE_CLASSES = (
100 | # 'django.middleware.cache.UpdateCacheMiddleware',
101 | 'django.middleware.common.CommonMiddleware',
102 | # 'django.middleware.cache.FetchFromCacheMiddleware',
103 | 'django.contrib.sessions.middleware.SessionMiddleware',
104 | 'django.middleware.csrf.CsrfViewMiddleware',
105 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
106 | 'django.contrib.messages.middleware.MessageMiddleware',
107 | )
108 |
109 | ROOT_URLCONF = 'urls'
110 |
111 | TEMPLATE_DIRS = (
112 | os.path.join(SITE_ROOT, 'dashboard/templates')
113 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
114 | # Always use forward slashes, even on Windows.
115 | # Don't forget to use absolute paths, not relative paths.
116 | )
117 |
118 | INSTALLED_APPS = (
119 | 'django.contrib.auth',
120 | 'django.contrib.contenttypes',
121 | 'django.contrib.sessions',
122 | 'django.contrib.sites',
123 | 'django.contrib.messages',
124 | 'django.contrib.staticfiles',
125 | 'django.contrib.admin',
126 | 'django.contrib.gis',
127 | 'django.contrib.humanize',
128 | 'dashboard',
129 | )
130 |
131 | # A sample logging configuration. The only tangible logging
132 | # performed by this configuration is to send an email to
133 | # the site admins on every HTTP 500 error.
134 | # See http://docs.djangoproject.com/en/dev/topics/logging for
135 | # more details on how to customize your logging configuration.
136 | LOGGING = {
137 | 'version': 1,
138 | 'disable_existing_loggers': False,
139 | 'handlers': {
140 | 'mail_admins': {
141 | 'level': 'ERROR',
142 | 'class': 'django.utils.log.AdminEmailHandler'
143 | }
144 | },
145 | 'loggers': {
146 | 'django.request': {
147 | 'handlers': ['mail_admins'],
148 | 'level': 'ERROR',
149 | 'propagate': True,
150 | },
151 | }
152 | }
153 |
154 | ###
155 | # Login URL
156 | ###
157 | LOGIN_URL = '/login/'
158 | LOGIN_REDIRECT_URL = '/admin/'
159 |
160 | ###
161 | # Local Settings
162 | ###
163 | from settings_local import *
164 |
--------------------------------------------------------------------------------
/settings_local.example.py:
--------------------------------------------------------------------------------
1 | # Django local settings
2 | DATABASES = {
3 | 'default': {
4 | 'ENGINE': 'django.contrib.gis.db.backends.postgis',
5 | 'NAME': '',
6 | 'USER': '',
7 | 'PASSWORD': '',
8 | 'HOST': '',
9 | 'PORT': '',
10 | }
11 | }
12 |
13 | # SECRET KEY
14 | SECRET_KEY = ''
15 |
16 | # Enable Geographic data
17 | ENABLE_GEO = True
18 |
19 | # Open311 City
20 | # See http://wiki.open311.org/GeoReport_v2/Servers
21 | CITY = {
22 | 'URL': 'https://open311.sfgov.org/dev/Open311/v2/requests.xml',
23 | 'PAGINATE': True,
24 | 'JURISDICTION': 'sfgov.org'
25 | }
26 |
--------------------------------------------------------------------------------
/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import patterns, url
2 |
3 | urlpatterns = patterns('',
4 | url(r'^$', 'dashboard.views.index'),
5 | url(r'^map/$', 'dashboard.views.map'),
6 |
7 | url(r'^street/$', 'dashboard.views.street_list'),
8 | url(r'^street/(?P\d+)/$',
9 | 'dashboard.views.street_view'),
10 | url(r'^street/(?P\d+).json',
11 | 'dashboard.views.street_view_json'),
12 |
13 | url(r'^neighborhood/$',
14 | 'dashboard.views.neighborhood_list'),
15 | url(r'^neighborhood/(?P\d+)/$',
16 | 'dashboard.views.neighborhood_detail'),
17 | url(r'^neighborhood/(?P\d+).json$',
18 | 'dashboard.views.neighborhood_detail_json'),
19 |
20 | url(r'^search/$',
21 | 'dashboard.views.street_search'),
22 |
23 | # API Calls
24 | url(r'^api/home/(?P\d+).json$',
25 | 'dashboard.views.index', {'is_json': True}),
26 |
27 | )
28 |
--------------------------------------------------------------------------------