├── .gitignore ├── .gitmodules ├── README.mkd ├── __init__.py ├── dashboard ├── __init__.py ├── decorators.py ├── fixtures │ ├── initial_data.json │ └── test.json ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── geojson.py │ │ ├── import.py │ │ ├── update_db.py │ │ └── utilities.py ├── models.py ├── scripts │ ├── __init__.py │ ├── bounding_box.py │ ├── calculate_centroid.py │ ├── extract_and_upload.py │ ├── extract_tiles.py │ ├── heatmap_blocks.py │ ├── heatmap_blocks_amazon.py │ ├── input │ │ └── ReadMe │ ├── nearest_street.py │ └── upload_to_s3.py ├── static │ ├── chosen-sprite.png │ ├── css │ │ ├── 960.css │ │ ├── chosen.css │ │ ├── handheld.css │ │ ├── open311.css │ │ ├── street_view_styles.css │ │ └── style.css │ └── js │ │ ├── autocomplete.js │ │ ├── charts │ │ └── dual_state_barchart.js │ │ ├── geo_detail.js │ │ ├── libs │ │ ├── chosen.jquery.min.js │ │ ├── d3.min.js │ │ ├── dd_belatedpng.js │ │ ├── jquery-1.5.1.js │ │ ├── jquery-1.5.1.min.js │ │ ├── modernizr-1.7.min.js │ │ ├── polymaps.min.js │ │ └── raphael-min.js │ │ ├── map.js │ │ ├── plugins.js │ │ ├── raycasting.js │ │ ├── script.js │ │ └── sparkline.html ├── templates │ ├── admin │ │ ├── city_view.html │ │ └── index.html │ ├── base │ │ ├── admin.html │ │ └── main.html │ ├── geo_detail.html │ ├── index.html │ ├── login.html │ ├── map.html │ ├── neighborhood_list.html │ ├── search.html │ └── street_list.html ├── tests.py ├── unit_tests.py ├── utils.py └── views.py ├── manage.py ├── requirements.txt ├── settings.py ├── settings_local.example.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | settings_local.py 3 | *.pyc 4 | src/ 5 | data/ 6 | dashboard/static/tiles/ 7 | epio.ini 8 | .epio-app 9 | dashboard/static/*.json 10 | *.swp 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dashboard/static/css/bootstrap"] 2 | path = dashboard/static/css/bootstrap 3 | url = https://github.com/twitter/bootstrap.git 4 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | Open311 Dashboard 2 | ================= 3 | We are creating an application to help people visualize and analyze 311 data on one unified dashboard. 4 | 5 | 6 | 7 | Non-Python Requirements 8 | ----------------------- 9 | You will need all of the libraries to run [GeoDjango](https://docs.djangoproject.com/en/dev/ref/contrib/gis/install/). If you are so inclined, you can install most of these with [homebrew](https://github.com/mxcl/homebrew). 10 | [KyngChaos](http://www.kyngchaos.com/software:frameworks) may also be a useful resource for MacOS X users. 11 | 12 | * [GEOS](http://trac.osgeo.org/geos/) 13 | * [GDAL](http://www.gdal.org/) 14 | * [PROJ.4](http://trac.osgeo.org/proj/) 15 | * [PostGIS](http://postgis.refractions.net/) 16 | * [Mapnik2](http://trac.mapnik.org/wiki/Mapnik2) (not explicitly required but this generates map tiles, see below.) 17 | 18 | Getting Started 19 | --------------- 20 | 1. Set up a database using the [PostGIS spatial database template](https://docs.djangoproject.com/en/dev/ref/contrib/gis/install/#spatialdb-template). 21 | 2. Rename settings_local.example.py to settings_local.py and set your db/secretkey variables. 22 | 3. `pip install -r requirements.txt` 23 | 4. `python manage.py syncdb` creates the database schema. 24 | 5. `python manage.py update_db` imports initial data. 25 | 6. Generate GeoJSON files with `python manage.py geojson` 26 | 7. `git submodule update --init` adds bootstrap submodule 27 | 8. `python manage.py runserver` 28 | 29 | A Word on Mapnik 30 | ---------------- 31 | [Mapnik2's Wiki](https://github.com/mapnik/mapnik/wiki/Mapnik2) has instructions for installing Mapnik2 on Mac OSX and Linux. 32 | Dane Springmeyer recently released [Mapnik2 binaries for Mac OSX](https://github.com/mapnik/mapnik/downloads). 33 | 34 | Database Update Script 35 | ---------------------- 36 | To update data from the Open311 API, run `python manage.py update_db`. 37 | This command has a few options that may be useful. The first argument is 38 | the end date, defaults to yesterday. The second argument is the number 39 | of days to download previous to that date. 40 | 41 | * Ex: `python manage.py updatedb 2011-07-01 30` will download the entire 42 | month of June. 43 | 44 | To set up automatic updates (midnight every night): 45 | 46 | 1. `crontab -e` 47 | 2. Insert `0 0 * * * /path/to/dir/manage.py update_db` 48 | 49 | API Access 50 | ---------- 51 | There are a number API calls to return JSON in various forms. 52 | 53 | * `/api` 54 | * `/tickets/(open|closed|both)` 55 | * `/[0-9]+` (n days previous to today) 56 | * `/YYYY-MM-DD(/[0-9]+)` (n days previous to the date) 57 | * `/YYYY-MM-DD/YYYY-MM-DD` (simple date range) 58 | * `/more_info/YYYY-MM-DD` (single day) 59 | * `/YYYY-MM-DD` (range) 60 | * `/list/YYYY-MM-DD` (singe day) 61 | * `/YYYY-MM-DD` (range) 62 | 63 | More Information 64 | ---------------- 65 | Visit our [project page](http://codeforamerica.org/?cfa_project=open311-dashboard). 66 | 67 | Join the [official Open311 Dashboard mailing list](http://groups.google.com/group/open311-dashboard). 68 | 69 | [![Code for America Tracker](http://stats.codeforamerica.org/codeforamerica/open311dashboard.png)](http://stats.codeforamerica.org/projects/open311dashboard) 70 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/__init__.py -------------------------------------------------------------------------------- /dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/dashboard/__init__.py -------------------------------------------------------------------------------- /dashboard/decorators.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | import json 4 | 5 | class ApiHandler(object): 6 | """When passed lists or dicts, it will return them in various serialized 7 | forms. Defaults to JSON, can also do JSONP.""" 8 | 9 | def __init__(self, func): 10 | self.func = func 11 | 12 | def __call__(self, *args, **kwargs): 13 | request = args[0] 14 | format = request.GET.get('format') 15 | 16 | response = self.func(*args, **kwargs) 17 | if format == 'jsonp': 18 | data = json.dumps(response) 19 | callback = request.GET.get('callback') 20 | 21 | if callback: 22 | data = "%s(%s)" % (callback, data) 23 | mime_type = 'application/javascript' 24 | else: 25 | mime_type = 'application/json' 26 | data = json.dumps(response) 27 | 28 | return HttpResponse(data, content_type=mime_type) 29 | -------------------------------------------------------------------------------- /dashboard/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, "model": "dashboard.city", "fields": {"paginated": false, "name": "San Francisco", "short_name": "", "url": "https://open311.sfgov.org/dev/Open311/v2/requests.xml", "jurisdiction_id": "sfgov.org", "api_key": ""}}] -------------------------------------------------------------------------------- /dashboard/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/dashboard/management/__init__.py -------------------------------------------------------------------------------- /dashboard/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/dashboard/management/commands/__init__.py -------------------------------------------------------------------------------- /dashboard/management/commands/geojson.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from open311dashboard.dashboard.models import Geography, Request, Street 3 | 4 | from django.db import connection 5 | import json 6 | 7 | # TODO: ABSTRACT THIS! 8 | class Command(BaseCommand): 9 | help = """ 10 | 11 | Grab relevant GeoJSON files to store and interact with the maps layer. We 12 | do not do this dynamically because the map layers are generated once a week 13 | and the JSON overlay should not interfere 14 | 15 | """ 16 | 17 | def handle(self, *args, **options): 18 | geojson = {"type": "FeatureCollection", 19 | "features":[] 20 | } 21 | # Select JSON 22 | 23 | cursor = connection.cursor() 24 | cursor.execute(""" 25 | SELECT a.* FROM( 26 | SELECT 27 | ST_AsGeoJSON(ST_Transform(ST_SetSRID(dashboard_street.line,900913),4326)), 28 | extract(epoch from avg(dashboard_request.updated_datetime - dashboard_request.requested_datetime)) as average, 29 | percent_rank() OVER (order by extract(epoch from avg(dashboard_request.updated_datetime - dashboard_request.requested_datetime))) as rank 30 | FROM dashboard_street 31 | LEFT OUTER JOIN 32 | dashboard_request ON (dashboard_street.id = dashboard_request.street_id) 33 | WHERE 34 | dashboard_request.status='Closed' AND 35 | dashboard_request.updated_datetime > dashboard_request.requested_datetime AND 36 | requested_datetime > '2010-12-31' AND 37 | dashboard_request.service_code = '024' 38 | GROUP BY dashboard_street.line 39 | ) AS a WHERE a.rank > .8 40 | """) 41 | rows = cursor.fetchall() 42 | 43 | for row in rows: 44 | geojson['features'].append({"type": "Feature", 45 | "geometry": json.loads(row[0]), 46 | "properties": { 47 | "percentile": "%s" % row[2] 48 | }}) 49 | f = open('dashboard/static/sidewalk_cleaning.json', 'w') 50 | f.write(json.dumps(geojson)) 51 | f.close() 52 | 53 | geojson['features'] = [] 54 | 55 | cursor.execute(""" 56 | SELECT a.* FROM( 57 | SELECT 58 | ST_AsGeoJSON(ST_Transform(ST_SetSRID(dashboard_street.line,900913),4326)), 59 | extract(epoch from avg(dashboard_request.updated_datetime - dashboard_request.requested_datetime)) as average, 60 | percent_rank() OVER (order by extract(epoch from avg(dashboard_request.updated_datetime - dashboard_request.requested_datetime))) as rank, 61 | dashboard_street.id 62 | FROM dashboard_street 63 | LEFT OUTER JOIN 64 | dashboard_request ON (dashboard_street.id = dashboard_request.street_id) 65 | WHERE 66 | dashboard_request.status='Closed' AND 67 | dashboard_request.updated_datetime > dashboard_request.requested_datetime AND 68 | requested_datetime > '2010-12-31' AND 69 | dashboard_request.service_code = '049' 70 | GROUP BY dashboard_street.line, dashboard_street.id 71 | ) AS a WHERE a.rank > .8 72 | """) 73 | rows = cursor.fetchall() 74 | for row in rows: 75 | geojson['features'].append({"type": "Feature", 76 | "geometry": json.loads(row[0]), 77 | "properties": { 78 | "percentile": "%s" % row[2], 79 | "id": "%s" % row[3] 80 | }}) 81 | f = open('dashboard/static/graffiti.json', 'w') 82 | f.write(json.dumps(geojson)) 83 | f.close() 84 | 85 | 86 | g = Geography.objects.all().transform() 87 | geojson['features'] = [] 88 | 89 | for shape in g: 90 | geojson['features'].append({"type": "Feature", 91 | "geometry": json.loads(shape.geo.simplify(.0003, 92 | preserve_topology=True).json), 93 | "properties": { 94 | "neighborhood": shape.name, 95 | "id": shape.id 96 | }}) 97 | 98 | h = open('dashboard/static/neighborhoods.json', 'w') 99 | h.write(json.dumps(geojson)) 100 | h.close() 101 | -------------------------------------------------------------------------------- /dashboard/management/commands/import.py: -------------------------------------------------------------------------------- 1 | from osgeo import ogr 2 | from django.contrib.gis.utils import LayerMapping 3 | 4 | from dashboard.models import Street 5 | 6 | ogr.UseExceptions() 7 | shapefile = '' 8 | 9 | source = ogr.Open(shapefile, 1) 10 | 11 | city_id_def = ogr.FieldDefn('CITY_ID', ogr.OFTInteger) 12 | layer = source.GetLayer() 13 | layer.CreateField(city_id_def) 14 | 15 | for feature in layer: 16 | feature.SetField('CITY_ID', 1) 17 | layer.SetFeature(feature) 18 | print "%s : %s" % (feature.GetField('STREETN_GC'), feature.GetField('CITY_ID')) 19 | 20 | 21 | mapping = {'line': 'LINESTRING', 22 | 'street_name': 'STREETN_GC', 23 | 'left_low_address': 'LF_FADD', 24 | 'left_high_address': 'LF_TOADD', 25 | 'right_low_address': 'RT_FADD', 26 | 'right_high_address': 'RT_TOADD', 27 | 'city': {'id': 'CITY_ID'}} 28 | 29 | lm = LayerMapping(Street, shapefile, mapping) 30 | lm.save(verbose=True) 31 | -------------------------------------------------------------------------------- /dashboard/management/commands/update_db.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.core.exceptions import ValidationError 3 | from open311dashboard.dashboard.models import City, Request 4 | from dateutil import parser 5 | 6 | from optparse import make_option 7 | import urllib2 8 | import urllib 9 | import datetime as dt 10 | import xml.dom.minidom as dom 11 | 12 | ONE_DAY = dt.timedelta(days=1) 13 | 14 | def get_time_range(on_day=None): 15 | if on_day is None: 16 | on_day = dt.datetime.utcnow() - ONE_DAY 17 | 18 | # End at the begining of on_day; start at the beginning of the previous 19 | # day. 20 | end = on_day.replace(hour=0, minute=0, second=0, microsecond=0) 21 | start = end - ONE_DAY 22 | 23 | return (start, end) 24 | 25 | def parse_date(date_string): 26 | new_date = parser.parse(date_string) 27 | return new_date.strftime("%Y-%m-%d %I:%M") 28 | 29 | def validate_dt_value(datetime): 30 | """ 31 | Verify that the given datetime will not cause problems for the Open311 API. 32 | For the San Francisco Open311 API, start and end dates are ISO8601 strings, 33 | but they are expected to be a specific subset. 34 | """ 35 | if datetime.microsecond != 0: 36 | raise ValueError('Microseconds on datetime must be 0: %s' % datetime) 37 | 38 | if datetime.tzinfo is not None: 39 | raise ValueError('Tzinfo on datetime must be None: %s' % datetime) 40 | 41 | def get_requests_from_SF(start,end,page,city): 42 | """ 43 | Retrieve the requests from the San Francisco 311 API within the time range 44 | specified by the dates start and end. 45 | 46 | Returns a stream containing the content from the API call. 47 | """ 48 | 49 | validate_dt_value(start) 50 | validate_dt_value(end) 51 | 52 | #url = r'https://open311.sfgov.org/dev/Open311/v2/requests.xml' #dev 53 | url = city.url 54 | query_data = { 55 | 'start_date' : start.isoformat() + 'Z', 56 | 'end_date' : end.isoformat() + 'Z', 57 | 'jurisdiction_id' : city.jurisdiction_id, 58 | } 59 | 60 | if page > 0: 61 | query_data['page'] = page 62 | 63 | query_str = urllib.urlencode(query_data) 64 | print url + '?' + query_str 65 | 66 | requests_stream = urllib2.urlopen(url + '?' + query_str) 67 | return requests_stream 68 | 69 | def parse_requests_doc(stream): 70 | """ 71 | Converts the given file-like object, which presumably contains a service 72 | requests document, into a list of request dictionaries. 73 | """ 74 | 75 | import xml.dom 76 | 77 | xml_string = stream.read() 78 | 79 | columns = [] #holding columns for a day's worth of incident data 80 | values = [] #holding values for a day's worth of incident data 81 | 82 | try: 83 | requests_root = dom.parseString(xml_string).documentElement 84 | except ExpatError: 85 | print(xml_string) 86 | raise 87 | 88 | if len(requests_root.childNodes) < 1: 89 | return False 90 | 91 | for request_node in requests_root.childNodes: 92 | indiv_columns_list = [] 93 | indiv_values_list = [] 94 | 95 | if request_node.nodeType != xml.dom.Node.ELEMENT_NODE: 96 | continue 97 | 98 | if request_node.tagName != 'request': 99 | raise Exception('Unexpected node: %s' % requests_root.toprettyxml()) 100 | 101 | for request_attr in request_node.childNodes: 102 | if request_attr.childNodes: 103 | if request_attr.tagName.find('datetime') > -1: 104 | request_attr.childNodes[0].data = parse_date(request_attr.childNodes[0].data) 105 | 106 | if request_attr.tagName in Request._meta.get_all_field_names(): 107 | indiv_columns_list.append(request_attr.tagName) 108 | indiv_values_list.append(request_attr.childNodes[0].data) 109 | 110 | columns.append(indiv_columns_list) 111 | values.append(indiv_values_list) 112 | return (columns,values) 113 | 114 | def insert_data(requests, city): 115 | ''' 116 | Takes the requests tuple, turns it into a dictionary, and saves it to the 117 | Requests model in django. 118 | ''' 119 | 120 | columns,values = requests 121 | 122 | for i in range(len(values)): 123 | 124 | # Put the key-value pairs into a dictionary and then an arguments list. 125 | request_dict = dict(zip(columns[i], values[i])) 126 | 127 | # Check if the record already exists. 128 | try: 129 | exists = Request.objects.get(service_request_id = request_dict['service_request_id'], 130 | service_code = request_dict['service_code']) 131 | request_dict['id'] = exists.id 132 | except: 133 | pass 134 | 135 | r = Request(**request_dict) 136 | 137 | # Hardcoded for now. 138 | r.city_id = city.id 139 | 140 | try: 141 | r.save() 142 | print "Successfully saved %s" % r.service_request_id 143 | except ValidationError, e: 144 | raise CommandError('Request "%s" does not validate correctly\n %s' % 145 | (r.service_request_id, e)) 146 | 147 | def process_requests(start, end, page, city): 148 | requests_stream = get_requests_from_SF(start, end, page, city) 149 | requests = parse_requests_doc(requests_stream) 150 | 151 | if requests != False: 152 | insert_data(requests, city) 153 | 154 | if page != 0: 155 | page = page+1 156 | process_requests(start, end, page, city) 157 | return requests 158 | 159 | def handle_open_requests(city): 160 | url = city.url 161 | open_requests = Request.objects.all().filter(status__iexact="open") 162 | length = len(open_requests) 163 | print "Checking %d tickets for changed status" % length 164 | 165 | for index in xrange(0, length, 10): 166 | data = [] 167 | for i in xrange(0, 10): 168 | data.append(open_requests[index + i].service_request_id) 169 | 170 | query_data = { 171 | 'jurisdiction_id': city.jurisdiction_id, 172 | 'service_request_id': ','.join(data) 173 | } 174 | 175 | query_str = urllib.urlencode(query_data) 176 | print url + '?' + query_str 177 | 178 | requests_stream = urllib2.urlopen(url + '?' + query_str) 179 | try: 180 | print "Parsing open docs" 181 | requests = parse_requests_doc(requests_stream) 182 | print "Saving..." 183 | insert_data(requests) 184 | except: 185 | print "Could not process updates." 186 | 187 | # At runtime... 188 | class Command(BaseCommand): 189 | option_list = BaseCommand.option_list + ( 190 | make_option('--checkopen', dest='open', 191 | default=False, help="Boolean to check open tickets"), 192 | make_option('--default', dest='default', 193 | default=True, help="Boolean to execute default functionality"), 194 | ) 195 | 196 | help = """Update and seed the database from data retrieved from the API. 197 | Makes calls one day at a time""" 198 | 199 | def handle(self, *args, **options): 200 | cities = City.objects.all() 201 | 202 | for city in cities: 203 | if options['default'] is True: 204 | if len(args) >= 1: 205 | start, end = get_time_range(dt.datetime.strptime(args[0], '%Y-%m-%d')) 206 | else: 207 | start, end = get_time_range() 208 | 209 | if len(args) >= 2: 210 | num_days = int(args[1]) 211 | print(args[1]) 212 | else: 213 | num_days = 1 214 | 215 | if city.paginated: 216 | page = 1 217 | else: 218 | page = False 219 | 220 | for _ in xrange(num_days): 221 | requests = process_requests(start, end, page, city) 222 | 223 | start -= ONE_DAY 224 | end -= ONE_DAY 225 | 226 | print start 227 | 228 | if options['open'] is True: 229 | handle_open_requests() 230 | 231 | 232 | -------------------------------------------------------------------------------- /dashboard/management/commands/utilities.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from dateutil import parser 3 | 4 | ONE_DAY = dt.timedelta(days=1) 5 | 6 | def get_time_range(on_day=None): 7 | """ 8 | Calculate a return a tuple of datetimes that are exactly 24 9 | hours apart, from midnight on the day passed in to the 10 | midnight prior. If the passed in value is None, then use 11 | datetime.utcnow() by default. 12 | """ 13 | 14 | # ensure that on_day is defaulted to the previous day 15 | if on_day is None: 16 | on_day = dt.datetime.utcnow() - ONE_DAY 17 | 18 | # End at the begining of on_day; start at the beginning of the previous 19 | # day relative to on_day. 20 | end = on_day.replace(hour=0, minute=0, second=0, microsecond=0) 21 | start = end - ONE_DAY 22 | 23 | # return tuple of start and end 24 | return (start, end) 25 | 26 | def transform_date(date_string): 27 | """ 28 | All Open311 date/time fields must be formatted in a common 29 | subset of ISO 8601 as per the w3 note. Timezone information 30 | (either Z meaning UTC, or an HH:MM offset from UTC) must be included. 31 | This method parses the Open311 date and transforms it into a simpler 32 | format and returns a string formatted as YYYY-MM-DD HH:MM. 33 | """ 34 | 35 | new_date = parser.parse(date_string) 36 | return new_date.strftime("%Y-%m-%d %I:%M") 37 | 38 | 39 | # TODO: 40 | # Why is this test done every time? Why can't it be a unit test? 41 | # If microsends are non-zero and tzinfo is not None the first time 42 | # then why would it ever change? 43 | # The comments indicate that this is for the SF Open 311 API, is this 44 | # really SF specific or should it be done for every endpoint 45 | def validate_dt_value(datetime): 46 | """ 47 | Verify that the given datetime will not cause problems for the Open311 API. 48 | For the San Francisco Open311 API, start and end dates are ISO8601 strings, 49 | but they are expected to be a specific subset. 50 | """ 51 | 52 | if datetime.microsecond != 0: 53 | raise ValueError('Microseconds on datetime must be 0: %s' % datetime) 54 | 55 | if datetime.tzinfo is not None: 56 | raise ValueError('Tzinfo on datetime must be None: %s' % datetime) 57 | 58 | 59 | -------------------------------------------------------------------------------- /dashboard/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db import models 2 | from django.contrib.gis.geos import Point 3 | from django.contrib.gis.measure import Distance as D 4 | from open311dashboard.settings import ENABLE_GEO 5 | 6 | class Request(models.Model): 7 | """ 8 | 9 | The actual meat-n-potatoes of the 311 dashboard, all the data. 10 | Implementations are different so most of these fields are optional. 11 | 12 | Optional: PostGIS component set in settings.py 13 | 14 | """ 15 | service_request_id = models.CharField(max_length=200) 16 | status = models.CharField(max_length=10) 17 | status_notes = models.TextField(blank=True, null=True) 18 | service_name = models.CharField(max_length=100) 19 | service_code = models.CharField(max_length=100, blank=True, null=True) 20 | description = models.TextField(blank=True, null=True) 21 | agency_responsible = models.CharField(max_length=255, blank=True, null=True) 22 | service_notice = models.CharField(max_length=255, blank=True, null=True) 23 | requested_datetime = models.DateTimeField() 24 | updated_datetime = models.DateTimeField(null=True, blank=True) 25 | expected_datetime = models.DateTimeField(null=True, blank=True) 26 | address = models.CharField(max_length=255) 27 | address_id = models.IntegerField(blank=True, null=True) 28 | zipcode = models.IntegerField(blank=True, null=True) 29 | lat = models.FloatField() 30 | long = models.FloatField() 31 | media_url = models.URLField(blank=True, null=True) 32 | 33 | city = models.ForeignKey('City') 34 | 35 | def get_service_name(self): 36 | return self.service_name.replace('_', ' ') 37 | 38 | 39 | # Super top secret geographic data. 40 | if ENABLE_GEO is True: 41 | geo_point = models.PointField(srid=900913, null=True) 42 | street = models.ForeignKey('Street', null=True) 43 | objects = models.GeoManager() 44 | 45 | def save(self): 46 | if (float(self.long) != 0.0 and float(self.lat) != 0.0): 47 | # Save the geo_point. 48 | point = Point(float(self.long), float(self.lat), srid=4326) 49 | point.transform(900913) 50 | 51 | self.geo_point = point 52 | 53 | # Lookup the nearest street 54 | street = Street.objects.filter(line__dwithin=(point, D(m=100))) \ 55 | .distance(point).order_by('distance')[:1] 56 | 57 | if len(street) > 0: 58 | self.street = street[0] 59 | 60 | super(Request, self).save() 61 | 62 | # class Service(models.Model): 63 | """ 64 | 65 | In a perfect world, this would be related to each Request but separate 66 | implementations are, again, different. 67 | 68 | """ 69 | # service_code = models.CharField(max_length=100) 70 | # metadata = models.CharField(max_length=100) 71 | # type = models.CharField(max_length=50) 72 | # keywords = models.TextField(blank=True, null=True) 73 | # group = models.CharField(max_length=100) 74 | # service_name = models.CharField(max_length=100) 75 | # description = models.TextField() 76 | 77 | # city = models.ForeignKey('City') 78 | # street = models.ForeignKey('Street') 79 | 80 | class City(models.Model): 81 | """ 82 | 83 | Give an ID to each city so everything can relate. 84 | 85 | """ 86 | name = models.CharField(max_length=100) 87 | short_name = models.CharField(max_length=50) 88 | api_key = models.CharField(max_length=255, blank=True, null=True) 89 | url = models.CharField(max_length=255) 90 | jurisdiction_id = models.CharField(max_length=100) 91 | paginated = models.BooleanField() 92 | 93 | def natural_key(self): 94 | return self.name 95 | 96 | if ENABLE_GEO is True: 97 | class Geography(models.Model): 98 | """ 99 | 100 | You can import any geographical shapes you want here and associate them 101 | with a city. 102 | 103 | """ 104 | name = models.CharField(max_length=25) 105 | geo = models.MultiPolygonField(srid=900913) 106 | 107 | city = models.ForeignKey('City') 108 | #geo_type = models.ForeignKey('GeographyType') 109 | 110 | objects = models.GeoManager() 111 | 112 | def __unicode__(self): 113 | return self.name 114 | 115 | def get_absolute_url(self): 116 | return "/neighborhood/%i/" % self.id 117 | 118 | # class GeographyType(models.Model): 119 | """ 120 | 121 | Ex: Neighborhood, Congressional Districts... 122 | 123 | """ 124 | # name = models.CharField(max_length=25) 125 | 126 | # Thank @ravoreyer for recommending this. 127 | # city = models.ForeignKey('City') 128 | 129 | class Street(models.Model): 130 | """ 131 | 132 | Street centerline data. 133 | 134 | """ 135 | street_name = models.CharField(max_length=100) 136 | line = models.LineStringField(srid=900913) 137 | city = models.ForeignKey("City") 138 | 139 | left_low_address = models.IntegerField(default=0) 140 | left_high_address = models.IntegerField(default=0) 141 | right_low_address = models.IntegerField(default=0) 142 | right_high_address = models.IntegerField(default=0) 143 | 144 | objects = models.GeoManager() 145 | 146 | def __unicode__(self): 147 | return self.street_name 148 | 149 | def natural_key(self): 150 | return self.street_name 151 | 152 | def get_absolute_url(self): 153 | return "/street/%i/" % self.id 154 | -------------------------------------------------------------------------------- /dashboard/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/dashboard/scripts/__init__.py -------------------------------------------------------------------------------- /dashboard/scripts/bounding_box.py: -------------------------------------------------------------------------------- 1 | def create_bounding_box(points): 2 | #initialize 3 | lon_init = points[0]['lon'] 4 | lat_init = points[0]['lat'] 5 | 6 | minLon = lon_init 7 | maxLon = lon_init 8 | minLat = lat_init 9 | maxLat = lat_init 10 | 11 | for i in xrange(1,len(points)): 12 | if minLon > points[i]['lon']: 13 | minLon = points[i]['lon'] 14 | if maxLon < points[i]['lon']: 15 | maxLon = points[i]['lon'] 16 | if minLat > points[i]['lat']: 17 | minLat = points[i]['lat'] 18 | if maxLat < points[i]['lat']: 19 | maxLat = points[i]['lat'] 20 | 21 | bounding_box = [{'lat':minLat,'lon':minLon},{'lat':maxLat,'lon':maxLon}] 22 | print bounding_box 23 | return bounding_box 24 | 25 | if __name__ == 'main': 26 | #Create array of test points 27 | points = [{'lat': 37.77017,'lon':-122.41996},{'lat': 37.77559,'lon':-122.41516},{'lat':37.77858,'lon':-122.42614}] 28 | create_bounding_box(points) -------------------------------------------------------------------------------- /dashboard/scripts/calculate_centroid.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | import urllib 3 | import json as simplejson 4 | #2-D approximation 5 | def compute_area_of_polygon(polygon_points): 6 | area = 0 7 | num_of_vertices = len(polygon_points) 8 | j = num_of_vertices - 1 9 | 10 | for i in xrange(num_of_vertices): 11 | point1 = polygon_points[i] 12 | point2 = polygon_points[j] 13 | 14 | area = area + point1[0]*point2[1] 15 | area = area - point1[1]*point2[0] 16 | 17 | j = i 18 | 19 | area = .5 * area 20 | 21 | return area 22 | 23 | def compute_centroid(polygon_points): 24 | num_of_vertices = len(polygon_points) 25 | j = num_of_vertices - 1 26 | x = 0 27 | y = 0 28 | 29 | for i in xrange(num_of_vertices): 30 | point1 = polygon_points[i] 31 | point2 = polygon_points[j] 32 | 33 | diff = point1[0]*point2[1] - point2[0]*point1[1] 34 | 35 | x = x + diff * (point1[0]+point2[0]) 36 | y = y + diff * (point1[1]+point2[1]) 37 | 38 | j = i 39 | 40 | factor = 6 * compute_area_of_polygon(polygon_points) 41 | 42 | centroid = [x/factor,y/factor] 43 | 44 | print 'The centroid of the polygon is', centroid 45 | 46 | return centroid 47 | 48 | if __name__ == '__main__': 49 | script,input_file = argv 50 | 51 | geojson_url = input_file 52 | geojson = simplejson.load(urllib.urlopen(geojson_url)) 53 | 54 | for i in xrange(len(geojson['features'])): 55 | print i 56 | if len(geojson['features'][i]['geometry']['coordinates'][0]) > 1: 57 | polygon_points = geojson['features'][i]['geometry']['coordinates'][0] 58 | else: 59 | polygon_points = geojson['features'][i]['geometry']['coordinates'][0][0] 60 | 61 | compute_centroid(polygon_points) 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /dashboard/scripts/extract_and_upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | import boto 5 | import sys 6 | from boto.s3.key import Key 7 | 8 | #Use this if you want to use the create_bucket method 9 | #from boto import s3 10 | 11 | def percent_cb(complete, total): 12 | sys.stdout.write('.') 13 | sys.stdout.flush() 14 | 15 | def extract_tiles(): 16 | """ 17 | Shoutout to @rosskarchner: https://gist.github.com/837851 18 | Extracting images out of mbtiles. Creating folders and filenames. 19 | Works with mbtiles exported out of TileMill 0.4.2. 20 | """ 21 | 22 | #Connect to the database 23 | connection = sqlite3.connect('census_blocks.mbtiles') 24 | 25 | #Get everything out of the flat file 26 | pieces = connection.execute('select * from tiles').fetchall() 27 | 28 | for piece in pieces: 29 | #the image is a png 30 | zoom_level, row, column, image = piece 31 | 32 | try: 33 | os.makedirs('%s/%s/' % (zoom_level,row)) 34 | except: 35 | pass 36 | tile = open('%s/%s/%s.png' % (zoom_level, row, column), 'wb') 37 | tile.write(image) 38 | tile.close() 39 | 40 | if to_upload: 41 | upload_to_s3() 42 | 43 | def upload_to_s3(): 44 | """ 45 | Assists you in uploading a set of directories/files to S3. Assumes that your S3 bucket 46 | has already been created. Use the boto's create_bucket method if you don't have an existing bucket. 47 | """ 48 | AWS_ACCESS_KEY_ID = 'Your AWS Access Key ID' 49 | AWS_SECRET_ACCESS_KEY = 'Your AWS Secret Access Key' 50 | 51 | #This is for making an extremely unique bucket name. 52 | #bucket_name = AWS_ACCESS_KEY_ID.lower() + 'Your desired bucket name' 53 | 54 | bucket_name = 'Your existing bucket name' 55 | 56 | #We connect! 57 | conn = boto.connect_s3(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) 58 | 59 | #Use this if you want to create a bucket 60 | #bucket = conn.create_bucket(bucket_name,location=s3.connection.Location.DEFAULT) 61 | 62 | #Connect to our existing bucket 63 | bucket = conn.get_bucket(bucket_name) 64 | 65 | #the base directory 66 | directory = 'base-directory' 67 | 68 | k = Key(bucket) 69 | 70 | for root, dirs, files in os.walk(directory): 71 | for f in files: 72 | print 'Uploading %s/%s to Amazon bucket %s' % (root, f, bucket_name) 73 | 74 | file_name = root + '/' + f 75 | 76 | k.key = file_name 77 | k.set_contents_from_filename(file_name,cb=percent_cb,num_cb=10) 78 | 79 | if __name__ == '__main__': 80 | """ 81 | Pass is in Boolean variable on the command line to specify whether you want to upload your tiles to Amazon S3. 82 | to_upload is a Boolean variable. 83 | """ 84 | script,to_upload = argv 85 | extract_tiles() 86 | 87 | -------------------------------------------------------------------------------- /dashboard/scripts/extract_tiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | mbtiles_filename = 'filename.mbtiles' 5 | 6 | def extract_tiles(): 7 | """ 8 | Shoutout to @rosskarchner: https://gist.github.com/837851 9 | Extracting images out of mbtiles. Creating folders and filenames. 10 | Works with mbtiles exported out of TileMill v0.4.2. 11 | """ 12 | 13 | #Connect to the database 14 | connection = sqlite3.connect(mbtiles_filename) 15 | 16 | #Get everything out of the flat file 17 | pieces = connection.execute('select * from tiles').fetchall() 18 | 19 | for piece in pieces: 20 | #the image is a png 21 | zoom_level, row, column, image = piece 22 | 23 | try: 24 | os.makedirs('%s/%s/' % (zoom_level,row)) 25 | except: 26 | pass 27 | tile = open('%s/%s/%s.png' % (zoom_level, row, column), 'wb') 28 | tile.write(image) 29 | tile.close() 30 | 31 | extract_tiles() 32 | 33 | 34 | -------------------------------------------------------------------------------- /dashboard/scripts/heatmap_blocks.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import json as simplejson 3 | import math 4 | import pprint 5 | 6 | service_requests_url = 'input/service_requests.json' 7 | blocks_url = 'input/sf_blocks.json' 8 | 9 | #def compute_rect_bounds(): 10 | 11 | def inside_rect_bounds(polygon_bounds,request_location): 12 | 13 | #initialize 14 | lon_init = polygon_bounds[0][0][0] 15 | lat_init = polygon_bounds[0][0][1] 16 | 17 | minLon = lon_init 18 | maxLon = lon_init 19 | minLat = lat_init 20 | maxLat = lat_init 21 | 22 | request_lat = request_location[0] 23 | request_lon = request_location[1] 24 | 25 | for i in xrange(1,len(polygon_bounds[0])): 26 | if minLon > polygon_bounds[0][i][0]: 27 | minLon = polygon_bounds[0][i][0] 28 | if maxLon < polygon_bounds[0][i][0]: 29 | maxLon = polygon_bounds[0][i][0] 30 | if minLat > polygon_bounds[0][i][1]: 31 | minLat = polygon_bounds[0][i][1] 32 | if maxLat < polygon_bounds[0][i][1]: 33 | maxLat = polygon_bounds[0][i][1] 34 | 35 | #rect_bounds = [minLon,maxLon,minLat,maxLat] 36 | if request_lat >= minLat and request_lat <= maxLat and request_lon >= minLon and request_lon <= maxLon: 37 | return True 38 | else: 39 | return False 40 | 41 | def inside_polygon(polygon_bounds,request_location): 42 | #polygon_bounds is an array 43 | #request_location is an array, lat/long 44 | request_lat = request_location[0] 45 | request_lon = request_location[1] 46 | 47 | vertices_count = len(polygon_bounds[0]) 48 | inside = False 49 | #i = 0 50 | j = vertices_count - 1 51 | 52 | if inside_rect_bounds(polygon_bounds,request_location) == False: 53 | return False 54 | else: 55 | for i in xrange(vertices_count): 56 | vertexA = polygon_bounds[0][i] 57 | vertexB = polygon_bounds[0][j] 58 | 59 | if (vertexA[0] < request_lon and vertexB[0] >= request_lon) or (vertexB[0] < request_lon and vertexA[0] >= request_lon): 60 | if vertexA[1] + (((request_lon - vertexA[0]) / (vertexB[0] - vertexA[0])) * (vertexB[1] - vertexA[1])) < request_lat: 61 | inside = not inside 62 | j = i 63 | return inside 64 | 65 | #load in data 66 | service_requests = simplejson.load(urllib.urlopen(service_requests_url)) 67 | blocks = simplejson.load(urllib.urlopen(blocks_url)) 68 | 69 | #block_totals = [] #maps to 7386 blocks 70 | block_totals = [0]*len(blocks["features"]) 71 | 72 | for i in xrange(len(service_requests["rows"])): 73 | print i 74 | request_lat = service_requests["rows"][i]["value"]["lat"] 75 | request_lon = service_requests["rows"][i]["value"]["long"] 76 | request_location = [float(request_lat),float(request_lon)] 77 | 78 | #print request_location 79 | 80 | if math.fabs(request_location[0]) != 0 or math.fabs(request_location[1]) != 0: 81 | for j in xrange(len(blocks["features"])): 82 | polygon_bounds = blocks["features"][j]["geometry"]["coordinates"] 83 | 84 | if inside_polygon(polygon_bounds,request_location) == True: 85 | block_totals[j] = block_totals[j] + 1 86 | else: 87 | continue 88 | #print block_totals 89 | 90 | print 'block_totals: ', block_totals 91 | 92 | for i in xrange(len(block_totals)): 93 | blocks["features"][i]["properties"]["count"] = block_totals[i] 94 | 95 | f = open('output/block_with_counts.json','w') 96 | simplejson.dump(blocks,f) 97 | f.close() -------------------------------------------------------------------------------- /dashboard/scripts/heatmap_blocks_amazon.py: -------------------------------------------------------------------------------- 1 | #works! 2 | import urllib 3 | import json as simplejson 4 | import math 5 | import pprint 6 | import psycopg2 as pg 7 | 8 | service_requests_url = 'input/service_requests.json' 9 | blocks_url = 'input/sf_blocks.json' 10 | 11 | def connect_with_amazon(**kwargs): 12 | conn = pg.connect(**kwargs) 13 | cursor = conn.cursor() 14 | cursor.execute("""select lat, long from dashboard_request where lat > 0 and 15 | requested_datetime > '12-31-2010'""") 16 | 17 | return cursor.fetchall() #returns a list 18 | #def compute_rect_bounds(): 19 | 20 | def inside_rect_bounds(polygon_bounds,request_location): 21 | 22 | #initialize 23 | lon_init = polygon_bounds[0][0][0] 24 | lat_init = polygon_bounds[0][0][1] 25 | 26 | minLon = lon_init 27 | maxLon = lon_init 28 | minLat = lat_init 29 | maxLat = lat_init 30 | 31 | request_lat = request_location[0] 32 | request_lon = request_location[1] 33 | 34 | for i in xrange(1,len(polygon_bounds[0])): 35 | if minLon > polygon_bounds[0][i][0]: 36 | minLon = polygon_bounds[0][i][0] 37 | if maxLon < polygon_bounds[0][i][0]: 38 | maxLon = polygon_bounds[0][i][0] 39 | if minLat > polygon_bounds[0][i][1]: 40 | minLat = polygon_bounds[0][i][1] 41 | if maxLat < polygon_bounds[0][i][1]: 42 | maxLat = polygon_bounds[0][i][1] 43 | 44 | #rect_bounds = [minLon,maxLon,minLat,maxLat] 45 | if request_lat >= minLat and request_lat <= maxLat and request_lon >= minLon and request_lon <= maxLon: 46 | return True 47 | else: 48 | return False 49 | 50 | def inside_polygon(polygon_bounds,request_location): 51 | #polygon_bounds is an array 52 | #request_location is an array, lat/long 53 | request_lat = request_location[0] 54 | request_lon = request_location[1] 55 | 56 | vertices_count = len(polygon_bounds[0]) 57 | inside = False 58 | #i = 0 59 | j = vertices_count - 1 60 | 61 | if inside_rect_bounds(polygon_bounds,request_location) == False: 62 | return False 63 | else: 64 | for i in xrange(vertices_count): 65 | vertexA = polygon_bounds[0][i] 66 | vertexB = polygon_bounds[0][j] 67 | 68 | if (vertexA[0] < request_lon and vertexB[0] >= request_lon) or (vertexB[0] < request_lon and vertexA[0] >= request_lon): 69 | if vertexA[1] + (((request_lon - vertexA[0]) / (vertexB[0] - vertexA[0])) * (vertexB[1] - vertexA[1])) < request_lat: 70 | inside = not inside 71 | j = i 72 | return inside 73 | 74 | #load in data 75 | #service_requests = simplejson.load(urllib.urlopen(service_requests_url)) 76 | service_requests = connect_with_amazon(host='', database='', user='',password='') 77 | print service_requests[0][0] 78 | 79 | blocks = simplejson.load(urllib.urlopen(blocks_url)) 80 | 81 | #block_totals = [] #maps to 7386 blocks 82 | block_totals = [0]*len(blocks["features"]) 83 | 84 | for i in xrange(len(service_requests)): 85 | print i 86 | request_lat = service_requests[i][0] 87 | print request_lat 88 | request_lon = service_requests[i][1] 89 | request_location = [float(request_lat),float(request_lon)] 90 | 91 | #print request_location 92 | 93 | if math.fabs(request_location[0]) != 0 or math.fabs(request_location[1]) != 0: 94 | for j in xrange(len(blocks["features"])): 95 | polygon_bounds = blocks["features"][j]["geometry"]["coordinates"] 96 | 97 | if inside_polygon(polygon_bounds,request_location) == True: 98 | block_totals[j] = block_totals[j] + 1 99 | else: 100 | continue 101 | #print block_totals 102 | 103 | print 'block_totals: ', block_totals 104 | 105 | for i in xrange(len(block_totals)): 106 | blocks["features"][i]["properties"]["count"] = block_totals[i] 107 | 108 | f = open('output/block_with_counts_pg.json','w') 109 | simplejson.dump(blocks,f) 110 | f.close() 111 | -------------------------------------------------------------------------------- /dashboard/scripts/input/ReadMe: -------------------------------------------------------------------------------- 1 | Fixtures live here. To be added soon! 2 | -------------------------------------------------------------------------------- /dashboard/scripts/nearest_street.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import urllib 4 | import json as simplejson 5 | import pprint 6 | import math 7 | 8 | service_requests_url = 'input/service_requests.json' 9 | centerlines_url = 'input/centerlines.json' 10 | 11 | def lat_long_to_x_y(lat,lng): 12 | """Returns an array, a lng/lat pair that has been converted to x and y""" 13 | pair = [] 14 | sinLat = math.sin((lat*math.pi)/180.0) 15 | 16 | x = ((lng+180.0)/360.0) 17 | y = (.5 - math.log((1.0 + sinLat)/(1.0 - sinLat)) / (4.0*math.pi)) 18 | 19 | pair.append(x) 20 | pair.append(y) 21 | 22 | return pair 23 | 24 | """ 25 | Duplicating function: to be fixed later 26 | """ 27 | def lat_long_to_x_y_list(lng_lat): 28 | pair = [] 29 | sinLat = math.sin((lng_lat[1]*math.pi)/180.0) 30 | 31 | x = ((lng_lat[0]+180.0)/360.0) 32 | y = (.5 - math.log((1.0 + sinLat)/(1.0 - sinLat)) / (4.0*math.pi)) 33 | 34 | pair.append(x) 35 | pair.append(y) 36 | #print(lng_lat[1], lng_lat[0]); 37 | return pair 38 | 39 | def dot_product(v1,v2): 40 | """ 41 | Take the dot product of two vectors; v1 and v2 are arrays. 42 | """ 43 | return v1[0] * v2[0] + v1[1] * v2[1] 44 | 45 | """ 46 | Optimizations 47 | TODO: Coarse Grid 48 | Precompute everything for a given segment, save time by computing information about a segment once, instead of everytime you use a segment 49 | Can take out square roots 50 | """ 51 | def compute_distance(incident_x_y, segment_start, segment_end): 52 | deltaX_btwn_endpoints = segment_end[0] - segment_start[0] 53 | deltaY_btwn_endpoints = segment_end[1] - segment_start[1] 54 | 55 | segment_deltas = [deltaX_btwn_endpoints,deltaY_btwn_endpoints] 56 | 57 | deltaX_btwn_incident_and_segment_start = incident_x_y[0] - segment_start[0] 58 | deltaY_btwn_incident_and_segment_start = incident_x_y[1] - segment_start[1] 59 | 60 | incident_start_deltas = [deltaX_btwn_incident_and_segment_start, deltaY_btwn_incident_and_segment_start] 61 | 62 | """ 63 | t is a parameter of the line segment. We compute the value of t, where the incident point orthogonally projects to the extended line segment. 64 | If t is less than 0, it projects before the startpoint. If t is greater than 1, it projects after the endpoint. Otherwise, it projects interior to 65 | the line segment. 66 | """ 67 | t = dot_product(segment_deltas,incident_start_deltas) 68 | 69 | if t <= 0: 70 | #startpoint is closest to incident point 71 | return math.sqrt(dot_product(incident_start_deltas, incident_start_deltas)) 72 | #return dot_product(incident_start_deltas, incident_start_deltas) 73 | 74 | squared_length_of_segment_deltas = dot_product(segment_deltas,segment_deltas) 75 | 76 | if t >= squared_length_of_segment_deltas: 77 | #endpoint is closest to incident point 78 | 79 | """ 80 | compute incident_end_deltas 81 | """ 82 | deltaX_btwn_incident_and_segment_end = incident_x_y[0] - segment_end[0] 83 | deltaY_btwn_incident_and_segment_end = incident_x_y[1] - segment_end[1] 84 | 85 | incident_end_deltas = [deltaX_btwn_incident_and_segment_end,deltaY_btwn_incident_and_segment_end] 86 | 87 | return math.sqrt(dot_product(incident_end_deltas,incident_end_deltas)) 88 | #return dot_product(incident_end_deltas,incident_end_deltas) 89 | """ 90 | closest point is interior to segment 91 | """ 92 | interior_closest = dot_product(incident_start_deltas,incident_start_deltas) - ((t*t)/squared_length_of_segment_deltas) 93 | if interior_closest < 0: 94 | return 0 95 | else: 96 | return math.sqrt(interior_closest) 97 | #return dot_product(incident_start_deltas,incident_start_deltas) - ((t*t)/squared_length_of_segment_deltas) 98 | 99 | 100 | def process_data(): 101 | service_requests = simplejson.load(urllib.urlopen(service_requests_url)) 102 | centerlines = simplejson.load(urllib.urlopen(centerlines_url)) 103 | #print(len(centerlines["features"])) 104 | 105 | service_requests_x_y = [] 106 | month_list = [] 107 | request_list = [] 108 | 109 | for i in range(len(service_requests["rows"])): 110 | lat = float(service_requests["rows"][i]["value"]["lat"]) 111 | lng = float(service_requests["rows"][i]["value"]["long"]) 112 | 113 | if lat != 0.0 and lng != 0.0: 114 | service_requests_x_y.append(lat_long_to_x_y(lat,lng)) 115 | 116 | line_segments = [] 117 | 118 | for i in range(len(centerlines["features"])): 119 | sub_segments = centerlines["features"][i]["geometry"]["coordinates"] 120 | line_segments.append(sub_segments) 121 | 122 | line_segments_x_y = [] 123 | for i in range(len(line_segments)): 124 | line_segments_x_y.append(map(lat_long_to_x_y_list, line_segments[i])) 125 | 126 | street_index = 0 127 | response_time_average = [0] * len(line_segments_x_y) 128 | response_time_sum = [0] * len(line_segments_x_y) 129 | street_count = [0] * len(line_segments_x_y) 130 | 131 | for i in range(len(service_requests_x_y)-41000): 132 | print i 133 | distance = 200; 134 | for j in range(len(line_segments_x_y)): 135 | for k in range(len(line_segments_x_y[j])-1): 136 | computed_distance = compute_distance(service_requests_x_y[i],line_segments_x_y[j][k],line_segments_x_y[j][k+1]) 137 | if (distance > computed_distance): 138 | distance = computed_distance 139 | street_index = j 140 | 141 | 142 | #requests_by_street[street_index].append(request_list[i]) 143 | 144 | street_count[street_index] = street_count[street_index] + 1 145 | print street_count 146 | max_count_list = [] 147 | 148 | maximum = 0 149 | 150 | individual_counts = [] 151 | 152 | maximum = max(street_count) 153 | normalized_street_scores = map(lambda x: 100*math.log((1024/maximum)*x,2) if x > 0 else x,street_count) 154 | #sub_centerlines = {"type": "FeatureCollection","features":[]} 155 | for i in range(len(centerlines["features"])): 156 | centerlines["features"][i]["properties"]["score"] = street_count[i] #either or 157 | #centerlines["features"][i]["properties"]["score"] = normalized_street_scores[i] 158 | 159 | #print "sub_centerlines length: ",len(sub_centerlines["features"]) 160 | 161 | 162 | #f = open('output/scored_centerlines_sub_final.json','w') 163 | #simplejson.dump(sub_centerlines,f) 164 | #f.close() 165 | f2 = open('output/scored_centerlines_final.json','w') 166 | simplejson.dump(centerlines,f2) 167 | f2.close() 168 | process_data() -------------------------------------------------------------------------------- /dashboard/scripts/upload_to_s3.py: -------------------------------------------------------------------------------- 1 | #Authors: Michael Lawrence Evans + Joanne Cheng 2 | import os 3 | import sys 4 | import boto 5 | from boto.s3.key import Key 6 | 7 | #Use this if you want to use the create_bucket method 8 | #from boto import s3 9 | 10 | def percent_cb(complete, total): 11 | """Command line updates.""" 12 | sys.stdout.write('.') 13 | sys.stdout.flush() 14 | 15 | def upload_to_s3(): 16 | AWS_ACCESS_KEY_ID = 'Your AWS Access Key ID' 17 | AWS_SECRET_ACCESS_KEY = 'Your AWS Secret Access Key' 18 | 19 | #This is for making an extremely unique bucket name. 20 | #bucket_name = AWS_ACCESS_KEY_ID.lower() + 'Your desired bucket name' 21 | 22 | bucket_name = 'Your existing bucket name' 23 | 24 | #We connect! 25 | conn = boto.connect_s3(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) 26 | 27 | #Use this if you want to create a bucket 28 | #bucket = conn.create_bucket(bucket_name,location=s3.connection.Location.DEFAULT) 29 | 30 | #Connect to our existing bucket 31 | bucket = conn.get_bucket(bucket_name) 32 | 33 | #the base directory 34 | directory = 'base-directory' 35 | 36 | k = Key(bucket) 37 | 38 | for root, dirs, files in os.walk(directory): 39 | for f in files: 40 | print 'Uploading %s/%s to Amazon bucket %s' % (root, f, bucket_name) #debugging 41 | 42 | file_name = root + '/' + f 43 | 44 | k.key = file_name 45 | k.set_contents_from_filename(file_name,cb=percent_cb,num_cb=10) 46 | 47 | upload_to_s3() -------------------------------------------------------------------------------- /dashboard/static/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/open311dashboard/891229ac922f8032d90658e0eef10562e79df622/dashboard/static/chosen-sprite.png -------------------------------------------------------------------------------- /dashboard/static/css/960.css: -------------------------------------------------------------------------------- 1 | /* 2 | 960 Grid System ~ Core CSS. 3 | Learn more ~ http://960.gs/ 4 | 5 | Licensed under GPL and MIT. 6 | */ 7 | 8 | /* 9 | Forces backgrounds to span full width, 10 | even if there is horizontal scrolling. 11 | Increase this if your layout is wider. 12 | 13 | Note: IE6 works fine without this fix. 14 | */ 15 | 16 | body { 17 | min-width: 960px; 18 | } 19 | 20 | /* `Container 21 | ----------------------------------------------------------------------------------------------------*/ 22 | 23 | .container_12, 24 | .container_16 { 25 | margin-left: auto; 26 | margin-right: auto; 27 | width: 960px; 28 | } 29 | 30 | /* `Grid >> Global 31 | ----------------------------------------------------------------------------------------------------*/ 32 | 33 | .grid_1, 34 | .grid_2, 35 | .grid_3, 36 | .grid_4, 37 | .grid_5, 38 | .grid_6, 39 | .grid_7, 40 | .grid_8, 41 | .grid_9, 42 | .grid_10, 43 | .grid_11, 44 | .grid_12, 45 | .grid_13, 46 | .grid_14, 47 | .grid_15, 48 | .grid_16 { 49 | display: inline; 50 | float: left; 51 | margin-left: 10px; 52 | margin-right: 10px; 53 | } 54 | 55 | .push_1, .pull_1, 56 | .push_2, .pull_2, 57 | .push_3, .pull_3, 58 | .push_4, .pull_4, 59 | .push_5, .pull_5, 60 | .push_6, .pull_6, 61 | .push_7, .pull_7, 62 | .push_8, .pull_8, 63 | .push_9, .pull_9, 64 | .push_10, .pull_10, 65 | .push_11, .pull_11, 66 | .push_12, .pull_12, 67 | .push_13, .pull_13, 68 | .push_14, .pull_14, 69 | .push_15, .pull_15 { 70 | position: relative; 71 | } 72 | 73 | .container_12 .grid_3, 74 | .container_16 .grid_4 { 75 | width: 220px; 76 | } 77 | 78 | .container_12 .grid_6, 79 | .container_16 .grid_8 { 80 | width: 460px; 81 | } 82 | 83 | .container_12 .grid_9, 84 | .container_16 .grid_12 { 85 | width: 700px; 86 | } 87 | 88 | .container_12 .grid_12, 89 | .container_16 .grid_16 { 90 | width: 940px; 91 | } 92 | 93 | /* `Grid >> Children (Alpha ~ First, Omega ~ Last) 94 | ----------------------------------------------------------------------------------------------------*/ 95 | 96 | .alpha { 97 | margin-left: 0; 98 | } 99 | 100 | .omega { 101 | margin-right: 0; 102 | } 103 | 104 | /* `Grid >> 12 Columns 105 | ----------------------------------------------------------------------------------------------------*/ 106 | 107 | .container_12 .grid_1 { 108 | width: 60px; 109 | } 110 | 111 | .container_12 .grid_2 { 112 | width: 140px; 113 | } 114 | 115 | .container_12 .grid_4 { 116 | width: 300px; 117 | } 118 | 119 | .container_12 .grid_5 { 120 | width: 380px; 121 | } 122 | 123 | .container_12 .grid_7 { 124 | width: 540px; 125 | } 126 | 127 | .container_12 .grid_8 { 128 | width: 620px; 129 | } 130 | 131 | .container_12 .grid_10 { 132 | width: 780px; 133 | } 134 | 135 | .container_12 .grid_11 { 136 | width: 860px; 137 | } 138 | 139 | /* `Grid >> 16 Columns 140 | ----------------------------------------------------------------------------------------------------*/ 141 | 142 | .container_16 .grid_1 { 143 | width: 40px; 144 | } 145 | 146 | .container_16 .grid_2 { 147 | width: 100px; 148 | } 149 | 150 | .container_16 .grid_3 { 151 | width: 160px; 152 | } 153 | 154 | .container_16 .grid_5 { 155 | width: 280px; 156 | } 157 | 158 | .container_16 .grid_6 { 159 | width: 340px; 160 | } 161 | 162 | .container_16 .grid_7 { 163 | width: 400px; 164 | } 165 | 166 | .container_16 .grid_9 { 167 | width: 520px; 168 | } 169 | 170 | .container_16 .grid_10 { 171 | width: 580px; 172 | } 173 | 174 | .container_16 .grid_11 { 175 | width: 640px; 176 | } 177 | 178 | .container_16 .grid_13 { 179 | width: 760px; 180 | } 181 | 182 | .container_16 .grid_14 { 183 | width: 820px; 184 | } 185 | 186 | .container_16 .grid_15 { 187 | width: 880px; 188 | } 189 | 190 | /* `Prefix Extra Space >> Global 191 | ----------------------------------------------------------------------------------------------------*/ 192 | 193 | .container_12 .prefix_3, 194 | .container_16 .prefix_4 { 195 | padding-left: 240px; 196 | } 197 | 198 | .container_12 .prefix_6, 199 | .container_16 .prefix_8 { 200 | padding-left: 480px; 201 | } 202 | 203 | .container_12 .prefix_9, 204 | .container_16 .prefix_12 { 205 | padding-left: 720px; 206 | } 207 | 208 | /* `Prefix Extra Space >> 12 Columns 209 | ----------------------------------------------------------------------------------------------------*/ 210 | 211 | .container_12 .prefix_1 { 212 | padding-left: 80px; 213 | } 214 | 215 | .container_12 .prefix_2 { 216 | padding-left: 160px; 217 | } 218 | 219 | .container_12 .prefix_4 { 220 | padding-left: 320px; 221 | } 222 | 223 | .container_12 .prefix_5 { 224 | padding-left: 400px; 225 | } 226 | 227 | .container_12 .prefix_7 { 228 | padding-left: 560px; 229 | } 230 | 231 | .container_12 .prefix_8 { 232 | padding-left: 640px; 233 | } 234 | 235 | .container_12 .prefix_10 { 236 | padding-left: 800px; 237 | } 238 | 239 | .container_12 .prefix_11 { 240 | padding-left: 880px; 241 | } 242 | 243 | /* `Prefix Extra Space >> 16 Columns 244 | ----------------------------------------------------------------------------------------------------*/ 245 | 246 | .container_16 .prefix_1 { 247 | padding-left: 60px; 248 | } 249 | 250 | .container_16 .prefix_2 { 251 | padding-left: 120px; 252 | } 253 | 254 | .container_16 .prefix_3 { 255 | padding-left: 180px; 256 | } 257 | 258 | .container_16 .prefix_5 { 259 | padding-left: 300px; 260 | } 261 | 262 | .container_16 .prefix_6 { 263 | padding-left: 360px; 264 | } 265 | 266 | .container_16 .prefix_7 { 267 | padding-left: 420px; 268 | } 269 | 270 | .container_16 .prefix_9 { 271 | padding-left: 540px; 272 | } 273 | 274 | .container_16 .prefix_10 { 275 | padding-left: 600px; 276 | } 277 | 278 | .container_16 .prefix_11 { 279 | padding-left: 660px; 280 | } 281 | 282 | .container_16 .prefix_13 { 283 | padding-left: 780px; 284 | } 285 | 286 | .container_16 .prefix_14 { 287 | padding-left: 840px; 288 | } 289 | 290 | .container_16 .prefix_15 { 291 | padding-left: 900px; 292 | } 293 | 294 | /* `Suffix Extra Space >> Global 295 | ----------------------------------------------------------------------------------------------------*/ 296 | 297 | .container_12 .suffix_3, 298 | .container_16 .suffix_4 { 299 | padding-right: 240px; 300 | } 301 | 302 | .container_12 .suffix_6, 303 | .container_16 .suffix_8 { 304 | padding-right: 480px; 305 | } 306 | 307 | .container_12 .suffix_9, 308 | .container_16 .suffix_12 { 309 | padding-right: 720px; 310 | } 311 | 312 | /* `Suffix Extra Space >> 12 Columns 313 | ----------------------------------------------------------------------------------------------------*/ 314 | 315 | .container_12 .suffix_1 { 316 | padding-right: 80px; 317 | } 318 | 319 | .container_12 .suffix_2 { 320 | padding-right: 160px; 321 | } 322 | 323 | .container_12 .suffix_4 { 324 | padding-right: 320px; 325 | } 326 | 327 | .container_12 .suffix_5 { 328 | padding-right: 400px; 329 | } 330 | 331 | .container_12 .suffix_7 { 332 | padding-right: 560px; 333 | } 334 | 335 | .container_12 .suffix_8 { 336 | padding-right: 640px; 337 | } 338 | 339 | .container_12 .suffix_10 { 340 | padding-right: 800px; 341 | } 342 | 343 | .container_12 .suffix_11 { 344 | padding-right: 880px; 345 | } 346 | 347 | /* `Suffix Extra Space >> 16 Columns 348 | ----------------------------------------------------------------------------------------------------*/ 349 | 350 | .container_16 .suffix_1 { 351 | padding-right: 60px; 352 | } 353 | 354 | .container_16 .suffix_2 { 355 | padding-right: 120px; 356 | } 357 | 358 | .container_16 .suffix_3 { 359 | padding-right: 180px; 360 | } 361 | 362 | .container_16 .suffix_5 { 363 | padding-right: 300px; 364 | } 365 | 366 | .container_16 .suffix_6 { 367 | padding-right: 360px; 368 | } 369 | 370 | .container_16 .suffix_7 { 371 | padding-right: 420px; 372 | } 373 | 374 | .container_16 .suffix_9 { 375 | padding-right: 540px; 376 | } 377 | 378 | .container_16 .suffix_10 { 379 | padding-right: 600px; 380 | } 381 | 382 | .container_16 .suffix_11 { 383 | padding-right: 660px; 384 | } 385 | 386 | .container_16 .suffix_13 { 387 | padding-right: 780px; 388 | } 389 | 390 | .container_16 .suffix_14 { 391 | padding-right: 840px; 392 | } 393 | 394 | .container_16 .suffix_15 { 395 | padding-right: 900px; 396 | } 397 | 398 | /* `Push Space >> Global 399 | ----------------------------------------------------------------------------------------------------*/ 400 | 401 | .container_12 .push_3, 402 | .container_16 .push_4 { 403 | left: 240px; 404 | } 405 | 406 | .container_12 .push_6, 407 | .container_16 .push_8 { 408 | left: 480px; 409 | } 410 | 411 | .container_12 .push_9, 412 | .container_16 .push_12 { 413 | left: 720px; 414 | } 415 | 416 | /* `Push Space >> 12 Columns 417 | ----------------------------------------------------------------------------------------------------*/ 418 | 419 | .container_12 .push_1 { 420 | left: 80px; 421 | } 422 | 423 | .container_12 .push_2 { 424 | left: 160px; 425 | } 426 | 427 | .container_12 .push_4 { 428 | left: 320px; 429 | } 430 | 431 | .container_12 .push_5 { 432 | left: 400px; 433 | } 434 | 435 | .container_12 .push_7 { 436 | left: 560px; 437 | } 438 | 439 | .container_12 .push_8 { 440 | left: 640px; 441 | } 442 | 443 | .container_12 .push_10 { 444 | left: 800px; 445 | } 446 | 447 | .container_12 .push_11 { 448 | left: 880px; 449 | } 450 | 451 | /* `Push Space >> 16 Columns 452 | ----------------------------------------------------------------------------------------------------*/ 453 | 454 | .container_16 .push_1 { 455 | left: 60px; 456 | } 457 | 458 | .container_16 .push_2 { 459 | left: 120px; 460 | } 461 | 462 | .container_16 .push_3 { 463 | left: 180px; 464 | } 465 | 466 | .container_16 .push_5 { 467 | left: 300px; 468 | } 469 | 470 | .container_16 .push_6 { 471 | left: 360px; 472 | } 473 | 474 | .container_16 .push_7 { 475 | left: 420px; 476 | } 477 | 478 | .container_16 .push_9 { 479 | left: 540px; 480 | } 481 | 482 | .container_16 .push_10 { 483 | left: 600px; 484 | } 485 | 486 | .container_16 .push_11 { 487 | left: 660px; 488 | } 489 | 490 | .container_16 .push_13 { 491 | left: 780px; 492 | } 493 | 494 | .container_16 .push_14 { 495 | left: 840px; 496 | } 497 | 498 | .container_16 .push_15 { 499 | left: 900px; 500 | } 501 | 502 | /* `Pull Space >> Global 503 | ----------------------------------------------------------------------------------------------------*/ 504 | 505 | .container_12 .pull_3, 506 | .container_16 .pull_4 { 507 | left: -240px; 508 | } 509 | 510 | .container_12 .pull_6, 511 | .container_16 .pull_8 { 512 | left: -480px; 513 | } 514 | 515 | .container_12 .pull_9, 516 | .container_16 .pull_12 { 517 | left: -720px; 518 | } 519 | 520 | /* `Pull Space >> 12 Columns 521 | ----------------------------------------------------------------------------------------------------*/ 522 | 523 | .container_12 .pull_1 { 524 | left: -80px; 525 | } 526 | 527 | .container_12 .pull_2 { 528 | left: -160px; 529 | } 530 | 531 | .container_12 .pull_4 { 532 | left: -320px; 533 | } 534 | 535 | .container_12 .pull_5 { 536 | left: -400px; 537 | } 538 | 539 | .container_12 .pull_7 { 540 | left: -560px; 541 | } 542 | 543 | .container_12 .pull_8 { 544 | left: -640px; 545 | } 546 | 547 | .container_12 .pull_10 { 548 | left: -800px; 549 | } 550 | 551 | .container_12 .pull_11 { 552 | left: -880px; 553 | } 554 | 555 | /* `Pull Space >> 16 Columns 556 | ----------------------------------------------------------------------------------------------------*/ 557 | 558 | .container_16 .pull_1 { 559 | left: -60px; 560 | } 561 | 562 | .container_16 .pull_2 { 563 | left: -120px; 564 | } 565 | 566 | .container_16 .pull_3 { 567 | left: -180px; 568 | } 569 | 570 | .container_16 .pull_5 { 571 | left: -300px; 572 | } 573 | 574 | .container_16 .pull_6 { 575 | left: -360px; 576 | } 577 | 578 | .container_16 .pull_7 { 579 | left: -420px; 580 | } 581 | 582 | .container_16 .pull_9 { 583 | left: -540px; 584 | } 585 | 586 | .container_16 .pull_10 { 587 | left: -600px; 588 | } 589 | 590 | .container_16 .pull_11 { 591 | left: -660px; 592 | } 593 | 594 | .container_16 .pull_13 { 595 | left: -780px; 596 | } 597 | 598 | .container_16 .pull_14 { 599 | left: -840px; 600 | } 601 | 602 | .container_16 .pull_15 { 603 | left: -900px; 604 | } 605 | 606 | /* `Clear Floated Elements 607 | ----------------------------------------------------------------------------------------------------*/ 608 | 609 | /* http://sonspring.com/journal/clearing-floats */ 610 | 611 | .clear { 612 | clear: both; 613 | display: block; 614 | overflow: hidden; 615 | visibility: hidden; 616 | width: 0; 617 | height: 0; 618 | } 619 | 620 | /* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */ 621 | 622 | .clearfix:before, 623 | .clearfix:after, 624 | .container_12:before, 625 | .container_12:after, 626 | .container_16:before, 627 | .container_16:after { 628 | content: '.'; 629 | display: block; 630 | overflow: hidden; 631 | visibility: hidden; 632 | font-size: 0; 633 | line-height: 0; 634 | width: 0; 635 | height: 0; 636 | } 637 | 638 | .clearfix:after, 639 | .container_12:after, 640 | .container_16:after { 641 | clear: both; 642 | } 643 | 644 | /* 645 | The following zoom:1 rule is specifically for IE6 + IE7. 646 | Move to separate stylesheet if invalid CSS is a problem. 647 | */ 648 | 649 | .clearfix, 650 | .container_12, 651 | .container_16 { 652 | zoom: 1; 653 | } -------------------------------------------------------------------------------- /dashboard/static/css/chosen.css: -------------------------------------------------------------------------------- 1 | /* @group Base */ 2 | select.chzn-select { 3 | visibility: hidden; 4 | height: 28px !important; 5 | min-height: 28px !important; 6 | } 7 | .chzn-container { 8 | font-size: 13px; 9 | position: relative; 10 | display: inline-block; 11 | zoom: 1; 12 | *display: inline; 13 | } 14 | .chzn-container .chzn-drop { 15 | background: #fff; 16 | border: 1px solid #aaa; 17 | border-top: 0; 18 | position: absolute; 19 | top: 29px; 20 | left: 0; 21 | -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); 22 | -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); 23 | -o-box-shadow : 0 4px 5px rgba(0,0,0,.15); 24 | box-shadow : 0 4px 5px rgba(0,0,0,.15); 25 | z-index: 999; 26 | } 27 | /* @end */ 28 | 29 | /* @group Single Chosen */ 30 | .chzn-container-single .chzn-single { 31 | background-color: #fff; 32 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); 33 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); 34 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); 35 | background-image: -o-linear-gradient(top, #eeeeee 0%,#ffffff 50%); 36 | background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 50%); 37 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 38 | background-image: linear-gradient(top, #eeeeee 0%,#ffffff 50%); 39 | -webkit-border-radius: 4px; 40 | -moz-border-radius : 4px; 41 | border-radius : 4px; 42 | -moz-background-clip : padding; 43 | -webkit-background-clip: padding-box; 44 | background-clip : padding-box; 45 | border: 1px solid #aaa; 46 | display: block; 47 | overflow: hidden; 48 | white-space: nowrap; 49 | position: relative; 50 | height: 26px; 51 | line-height: 26px; 52 | padding: 0 0 0 8px; 53 | color: #444; 54 | text-decoration: none; 55 | } 56 | .chzn-container-single .chzn-single span { 57 | margin-right: 26px; 58 | display: block; 59 | overflow: hidden; 60 | white-space: nowrap; 61 | -o-text-overflow: ellipsis; 62 | -ms-text-overflow: ellipsis; 63 | -moz-binding: url('/xml/ellipsis.xml#ellipsis'); 64 | text-overflow: ellipsis; 65 | } 66 | .chzn-container-single .chzn-single div { 67 | -webkit-border-radius: 0 4px 4px 0; 68 | -moz-border-radius : 0 4px 4px 0; 69 | border-radius : 0 4px 4px 0; 70 | -moz-background-clip : padding; 71 | -webkit-background-clip: padding-box; 72 | background-clip : padding-box; 73 | background: #ccc; 74 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 75 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 76 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 77 | background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); 78 | background-image: -ms-linear-gradient(top, #cccccc 0%,#eeeeee 60%); 79 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#cccccc', endColorstr='#eeeeee',GradientType=0 ); 80 | background-image: linear-gradient(top, #cccccc 0%,#eeeeee 60%); 81 | border-left: 1px solid #aaa; 82 | position: absolute; 83 | right: 0; 84 | top: 0; 85 | display: block; 86 | height: 100%; 87 | width: 18px; 88 | } 89 | .chzn-container-single .chzn-single div b { 90 | background: url('/static/chosen-sprite.png') no-repeat 0 1px; 91 | display: block; 92 | width: 100%; 93 | height: 100%; 94 | } 95 | .chzn-container-single .chzn-search { 96 | padding: 3px 4px; 97 | margin: 0; 98 | white-space: nowrap; 99 | } 100 | .chzn-container-single .chzn-search input { 101 | background: #fff url('/static/chosen-sprite.png') no-repeat 100% -20px; 102 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 103 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 104 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 105 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 106 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 107 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 108 | background: url('/static/chosen-sprite.png') no-repeat 100% -20px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); 109 | margin: 1px 0; 110 | padding: 4px 20px 4px 5px; 111 | outline: 0; 112 | border: 1px solid #aaa; 113 | font-family: sans-serif; 114 | font-size: 1em; 115 | } 116 | .chzn-container-single .chzn-drop { 117 | -webkit-border-radius: 0 0 4px 4px; 118 | -moz-border-radius : 0 0 4px 4px; 119 | border-radius : 0 0 4px 4px; 120 | -moz-background-clip : padding; 121 | -webkit-background-clip: padding-box; 122 | background-clip : padding-box; 123 | } 124 | /* @end */ 125 | 126 | /* @group Multi Chosen */ 127 | .chzn-container-multi .chzn-choices { 128 | background-color: #fff; 129 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 130 | background-image: -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 131 | background-image: -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 132 | background-image: -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 133 | background-image: -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 134 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 135 | background-image: linear-gradient(top, #ffffff 85%,#eeeeee 99%); 136 | border: 1px solid #aaa; 137 | margin: 0; 138 | padding: 0; 139 | cursor: text; 140 | overflow: hidden; 141 | height: auto !important; 142 | height: 1%; 143 | position: relative; 144 | } 145 | .chzn-container-multi .chzn-choices li { 146 | float: left; 147 | list-style: none; 148 | } 149 | .chzn-container-multi .chzn-choices .search-field { 150 | white-space: nowrap; 151 | margin: 0; 152 | padding: 0; 153 | } 154 | .chzn-container-multi .chzn-choices .search-field input { 155 | color: #666; 156 | background: transparent !important; 157 | border: 0 !important; 158 | padding: 5px; 159 | margin: 1px 0; 160 | outline: 0; 161 | -webkit-box-shadow: none; 162 | -moz-box-shadow : none; 163 | -o-box-shadow : none; 164 | box-shadow : none; 165 | } 166 | .chzn-container-multi .chzn-choices .search-field .default { 167 | color: #999; 168 | } 169 | .chzn-container-multi .chzn-choices .search-choice { 170 | -webkit-border-radius: 3px; 171 | -moz-border-radius : 3px; 172 | border-radius : 3px; 173 | -moz-background-clip : padding; 174 | -webkit-background-clip: padding-box; 175 | background-clip : padding-box; 176 | background-color: #e4e4e4; 177 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #e4e4e4), color-stop(0.7, #eeeeee)); 178 | background-image: -webkit-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%); 179 | background-image: -moz-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%); 180 | background-image: -o-linear-gradient(bottom, #e4e4e4 0%, #eeeeee 70%); 181 | background-image: -ms-linear-gradient(top, #e4e4e4 0%,#eeeeee 70%); 182 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e4e4e4', endColorstr='#eeeeee',GradientType=0 ); 183 | background-image: linear-gradient(top, #e4e4e4 0%,#eeeeee 70%); 184 | color: #333; 185 | border: 1px solid #b4b4b4; 186 | line-height: 13px; 187 | padding: 3px 19px 3px 6px; 188 | margin: 3px 0 3px 5px; 189 | position: relative; 190 | } 191 | .chzn-container-multi .chzn-choices .search-choice span { 192 | cursor: default; 193 | } 194 | .chzn-container-multi .chzn-choices .search-choice-focus { 195 | background: #d4d4d4; 196 | } 197 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close { 198 | display: block; 199 | position: absolute; 200 | right: 5px; 201 | top: 6px; 202 | width: 8px; 203 | height: 9px; 204 | font-size: 1px; 205 | background: url('/static/chosen-sprite.png') right top no-repeat; 206 | } 207 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { 208 | background-position: right -9px; 209 | } 210 | .chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { 211 | background-position: right -9px; 212 | } 213 | /* @end */ 214 | 215 | /* @group Results */ 216 | .chzn-container .chzn-results { 217 | margin: 0 4px 4px 0; 218 | max-height: 190px; 219 | padding: 0 0 0 4px; 220 | position: relative; 221 | overflow-x: hidden; 222 | overflow-y: auto; 223 | } 224 | .chzn-container-multi .chzn-results { 225 | margin: -1px 0 0; 226 | padding: 0; 227 | } 228 | .chzn-container .chzn-results li { 229 | line-height: 80%; 230 | padding: 7px 7px 8px; 231 | margin: 0; 232 | list-style: none; 233 | } 234 | .chzn-container .chzn-results .active-result { 235 | cursor: pointer; 236 | } 237 | .chzn-container .chzn-results .highlighted { 238 | background: #3875d7; 239 | color: #fff; 240 | } 241 | .chzn-container .chzn-results li em { 242 | background: #feffde; 243 | font-style: normal; 244 | } 245 | .chzn-container .chzn-results .highlighted em { 246 | background: transparent; 247 | } 248 | .chzn-container .chzn-results .no-results { 249 | background: #f4f4f4; 250 | } 251 | .chzn-container .chzn-results .group-result { 252 | cursor: default; 253 | color: #999; 254 | font-weight: bold; 255 | } 256 | .chzn-container .chzn-results .group-option { 257 | padding-left: 20px; 258 | } 259 | .chzn-container-multi .chzn-drop .result-selected { 260 | display: none; 261 | } 262 | /* @end */ 263 | 264 | /* @group Active */ 265 | .chzn-container-active .chzn-single { 266 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 267 | -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); 268 | -o-box-shadow : 0 0 5px rgba(0,0,0,.3); 269 | box-shadow : 0 0 5px rgba(0,0,0,.3); 270 | border: 1px solid #5897fb; 271 | } 272 | .chzn-container-active .chzn-single-with-drop { 273 | border: 1px solid #aaa; 274 | -webkit-box-shadow: 0 1px 0 #fff inset; 275 | -moz-box-shadow : 0 1px 0 #fff inset; 276 | -o-box-shadow : 0 1px 0 #fff inset; 277 | box-shadow : 0 1px 0 #fff inset; 278 | background-color: #eee; 279 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); 280 | background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); 281 | background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); 282 | background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); 283 | background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); 284 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 285 | background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); 286 | -webkit-border-bottom-left-radius : 0; 287 | -webkit-border-bottom-right-radius: 0; 288 | -moz-border-radius-bottomleft : 0; 289 | -moz-border-radius-bottomright: 0; 290 | border-bottom-left-radius : 0; 291 | border-bottom-right-radius: 0; 292 | } 293 | .chzn-container-active .chzn-single-with-drop div { 294 | background: transparent; 295 | border-left: none; 296 | } 297 | .chzn-container-active .chzn-single-with-drop div b { 298 | background-position: -18px 1px; 299 | } 300 | .chzn-container-active .chzn-choices { 301 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 302 | -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); 303 | -o-box-shadow : 0 0 5px rgba(0,0,0,.3); 304 | box-shadow : 0 0 5px rgba(0,0,0,.3); 305 | border: 1px solid #5897fb; 306 | } 307 | .chzn-container-active .chzn-choices .search-field input { 308 | color: #111 !important; 309 | } 310 | /* @end */ 311 | 312 | /* @group Right to Left */ 313 | .chzn-rtl { direction:rtl;text-align: right; } 314 | .chzn-rtl .chzn-single { padding-left: 0; padding-right: 8px; } 315 | .chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; } 316 | .chzn-rtl .chzn-single div { 317 | left: 0; right: auto; 318 | border-left: none; border-right: 1px solid #aaaaaa; 319 | -webkit-border-radius: 4px 0 0 4px; 320 | -moz-border-radius : 4px 0 0 4px; 321 | border-radius : 4px 0 0 4px; 322 | } 323 | .chzn-rtl .chzn-choices li { float: right; } 324 | .chzn-rtl .chzn-choices .search-choice { padding: 3px 6px 3px 19px; margin: 3px 5px 3px 0; } 325 | .chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 5px; right: auto; background-position: right top;} 326 | .chzn-rtl.chzn-container-single .chzn-results { margin-left: 4px; margin-right: 0; padding-left: 0; padding-right: 4px; } 327 | .chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 20px; } 328 | .chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; } 329 | .chzn-rtl .chzn-search input { 330 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, #ffffff; 331 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 332 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 333 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 334 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 335 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 336 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 337 | background: url('/static/chosen-sprite.png') no-repeat -38px -20px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); 338 | padding: 4px 5px 4px 20px; 339 | } 340 | /* @end */ 341 | -------------------------------------------------------------------------------- /dashboard/static/css/handheld.css: -------------------------------------------------------------------------------- 1 | * { 2 | float: none; /* Screens are not big enough to account for floats */ 3 | background: #fff; /* As much contrast as possible */ 4 | color: #000; 5 | } 6 | 7 | /* Slightly reducing font size to reduce need to scroll */ 8 | body { font-size: 80%; } -------------------------------------------------------------------------------- /dashboard/static/css/open311.css: -------------------------------------------------------------------------------- 1 | div.topbar-wrapper { 2 | position: relative; 3 | height: 40px; 4 | margin: 5px 0 15px; 5 | } 6 | div .topbar { 7 | background-color:black; 8 | margin: 0 -20px; 9 | padding-left: 20px; 10 | padding-right: 20px; 11 | -moz-border-radius: 4px; 12 | border-radius: 4px; 13 | } 14 | 15 | body{ 16 | overflow:hidden; 17 | } 18 | .show-grid { 19 | margin-top: 20px; 20 | margin-bottom: 20px; 21 | } 22 | .show-grid .column, .show-grid .columns { 23 | text-align: center; 24 | -moz-border-radius: 3px; 25 | border-radius: 3px; 26 | } 27 | .search-select { 28 | color: #E6E6E6; 29 | background-color: darkGray; 30 | } 31 | 32 | #map{ 33 | -moz-border-radius: 5px; 34 | border-radius: 5px; 35 | height: 500px; 36 | background-color: #DCEAF4; 37 | } 38 | 39 | 40 | .ui-autocomplete,.ui-corner-all{ 41 | list-style-type: none; 42 | width: 250px; 43 | } 44 | -------------------------------------------------------------------------------- /dashboard/static/css/street_view_styles.css: -------------------------------------------------------------------------------- 1 | /*CONTAINERS*/ 2 | 3 | #nav { 4 | width: 990px; 5 | text-align: right; 6 | margin: 10px; 7 | } 8 | 9 | #map, #street-stats, #open-requests, #nearby { 10 | -moz-border-radius: 5px; 11 | -webkit-border-radius: 5px; 12 | border-radius: 5px; 13 | -khtml-border-radius: 5px; 14 | margin: 10px 0; 15 | width: 100%; 16 | float: left; 17 | background-color: white; 18 | padding-bottom:10px; 19 | } 20 | 21 | #map { 22 | height: 380px; 23 | padding-bottom:0px; 24 | } 25 | 26 | /*MAP*/ 27 | #geometry path { 28 | stroke:#666; 29 | stroke-linecap:round; 30 | } 31 | 32 | .street path { 33 | fill:none; 34 | stroke-width:5px; 35 | } 36 | 37 | .neighborhood path { 38 | stroke-width:3px; 39 | fill:#fff; 40 | fill-opacity:.4; 41 | } 42 | 43 | /*STREET STATS*/ 44 | 45 | #spark-line { 46 | width: 220px; 47 | height: 60px; 48 | background-color: white; 49 | margin: 10px auto; 50 | } 51 | 52 | #response-time { 53 | font-size: 32px; 54 | font-weight: bold; 55 | margin: 0; 56 | padding: 15px 0 0; 57 | text-align: center; 58 | } 59 | 60 | #top-requests .service-name { 61 | width:75%; 62 | } 63 | 64 | /*OPEN REQUESTS*/ 65 | #open-requests p { 66 | text-align: right; 67 | } 68 | 69 | #open-requests table .service-name { 70 | width:75%; 71 | } 72 | 73 | 74 | /*NEARBY*/ 75 | 76 | #nearby table td { 77 | background-color: white; 78 | padding: 8px 0; 79 | } 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /dashboard/static/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML5 ✰ Boilerplate 3 | * 4 | * style.css contains a reset, font normalization and some base styles. 5 | * 6 | * Credit is left where credit is due. 7 | * Much inspiration was taken from these projects: 8 | * - yui.yahooapis.com/2.8.1/build/base/base.css 9 | * - camendesign.com/design/ 10 | * - praegnanz.de/weblog/htmlcssjs-kickstart 11 | */ 12 | 13 | 14 | /** 15 | * html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline) 16 | * v1.6.1 2010-09-17 | Authors: Eric Meyer & Richard Clark 17 | * html5doctor.com/html-5-reset-stylesheet/ 18 | */ 19 | 20 | html, body, div, span, object, iframe, 21 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 22 | abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, 23 | small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, 24 | fieldset, form, label, legend, 25 | table, caption, tbody, tfoot, thead, tr, th, td, 26 | article, aside, canvas, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section, summary, 28 | time, mark, audio, video { 29 | margin: 0; 30 | padding: 0; 31 | border: 0; 32 | font-size: 100%; 33 | font: inherit; 34 | vertical-align: baseline; 35 | } 36 | 37 | article, aside, details, figcaption, figure, 38 | footer, header, hgroup, menu, nav, section { 39 | display: block; 40 | } 41 | 42 | blockquote, q { quotes: none; } 43 | 44 | blockquote:before, blockquote:after, 45 | q:before, q:after { content: ""; content: none; } 46 | 47 | ins { background-color: #ff9; color: #000; text-decoration: none; } 48 | 49 | mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } 50 | 51 | del { text-decoration: line-through; } 52 | 53 | abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } 54 | 55 | table { border-collapse: collapse; border-spacing: 0; } 56 | 57 | hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } 58 | 59 | input, select { vertical-align: middle; } 60 | 61 | 62 | /** 63 | * Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/ 64 | */ 65 | 66 | body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */ 67 | select, input, textarea, button { font:99% sans-serif; } 68 | 69 | /* Normalize monospace sizing: 70 | en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */ 71 | pre, code, kbd, samp { font-family: monospace, sans-serif; } 72 | 73 | 74 | /** 75 | * Minimal base styles. 76 | */ 77 | 78 | /* Always force a scrollbar in non-IE */ 79 | html { overflow-y: scroll; } 80 | 81 | /* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */ 82 | a:hover, a:active { outline: none; } 83 | 84 | ul, ol { margin-left: 2em; } 85 | ol { list-style-type: decimal; } 86 | 87 | /* Remove margins for navigation lists */ 88 | nav ul, nav li { margin: 0; list-style:none; list-style-image: none; } 89 | 90 | small { font-size: 85%; } 91 | strong, th { font-weight: bold; } 92 | 93 | td { vertical-align: top; } 94 | 95 | /* Set sub, sup without affecting line-height: gist.github.com/413930 */ 96 | sub, sup { font-size: 75%; line-height: 0; position: relative; } 97 | sup { top: -0.5em; } 98 | sub { bottom: -0.25em; } 99 | 100 | pre { 101 | /* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */ 102 | white-space: pre; white-space: pre-wrap; word-wrap: break-word; 103 | padding: 15px; 104 | } 105 | 106 | textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */ 107 | 108 | .ie6 legend, .ie7 legend { margin-left: -7px; } 109 | 110 | /* Align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */ 111 | input[type="radio"] { vertical-align: text-bottom; } 112 | input[type="checkbox"] { vertical-align: bottom; } 113 | .ie7 input[type="checkbox"] { vertical-align: baseline; } 114 | .ie6 input { vertical-align: text-bottom; } 115 | 116 | /* Hand cursor on clickable input elements */ 117 | label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; } 118 | 119 | /* Webkit browsers add a 2px margin outside the chrome of form elements */ 120 | button, input, select, textarea { margin: 0; } 121 | 122 | /* Colors for form validity */ 123 | input:valid, textarea:valid { } 124 | input:invalid, textarea:invalid { 125 | border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; 126 | } 127 | .no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; } 128 | 129 | 130 | /* These selection declarations have to be separate 131 | No text-shadow: twitter.com/miketaylr/status/12228805301 132 | Also: hot pink! */ 133 | ::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; } 134 | ::selection { background:#FF5E99; color:#fff; text-shadow: none; } 135 | 136 | /* j.mp/webkit-tap-highlight-color */ 137 | a:link { -webkit-tap-highlight-color: #FF5E99; } 138 | 139 | /* Make buttons play nice in IE: 140 | www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */ 141 | button { width: auto; overflow: visible; } 142 | 143 | /* Bicubic resizing for non-native sized IMG: 144 | code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */ 145 | .ie7 img { -ms-interpolation-mode: bicubic; } 146 | 147 | /** 148 | * You might tweak these.. 149 | */ 150 | 151 | body, select, input, textarea { 152 | /* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */ 153 | color: #444; 154 | /* Set your base font here, to apply evenly */ 155 | /* font-family: Georgia, serif; */ 156 | } 157 | 158 | /* Headers (h1, h2, etc) have no default font-size or margin; define those yourself */ 159 | h1, h2, h3, h4, h5, h6 { font-weight: bold; } 160 | 161 | a, a:active, a:visited { color: #607890; } 162 | a:hover { color: #036; } 163 | 164 | 165 | /** 166 | * Primary styles 167 | * 168 | * Author: Chris Barna and mevans. 169 | */ 170 | /*GENERAL STYLES*/ 171 | body { 172 | font-family: helvetica; 173 | background-color: #EDEDED; 174 | } 175 | 176 | h1 { 177 | font-size: 18px; 178 | font-weight: bold; 179 | padding: 20px 10px; 180 | } 181 | 182 | h2 { 183 | font-size: 14px; 184 | text-align: center; 185 | padding: 5px; 186 | } 187 | 188 | p { 189 | font-size: 12px; 190 | padding: 5px; 191 | } 192 | 193 | .caption { 194 | text-align: center; 195 | font-style: italic; 196 | font-size: 14px; 197 | margin: 0 15px; 198 | display: block; 199 | border-bottom: 1px solid #EDEDED; 200 | 201 | } 202 | 203 | /*TABLES*/ 204 | 205 | table { 206 | width: 90%; 207 | margin: 0 auto; 208 | font-size: 14px; 209 | } 210 | 211 | th { 212 | text-align: left; 213 | font-weight: bold; 214 | } 215 | 216 | td { 217 | padding: 5px 0; 218 | } 219 | 220 | tr { 221 | background-color: #fff; 222 | } 223 | 224 | tr:nth-child(2n) { 225 | background-color: #EDEDED; 226 | } 227 | 228 | #street-stats td:nth-child(2), #nearby td:nth-child(2) { 229 | text-align: right; 230 | } 231 | 232 | #container { 233 | min-width:960px; 234 | min-height:600px; 235 | height:100%; 236 | overflow:hidden; 237 | } 238 | 239 | #container header { 240 | background:#ccc; 241 | padding:5px 0px; 242 | } 243 | 244 | .border-radius { 245 | margin-top:10px; 246 | -moz-border-radius: 5px; 247 | -webkit-border-radius: 5px; 248 | border-radius: 5px; 249 | -khtml-border-radius: 5px; 250 | } 251 | 252 | .white { 253 | background-color:#fff; 254 | } 255 | 256 | #title { 257 | width:50%; 258 | float:left; 259 | } 260 | 261 | #title h1 { padding:0 5px; } 262 | 263 | #home-nav { 264 | width:49%; 265 | float:right; 266 | text-align:right; 267 | padding-top:8px; 268 | padding-right:1%; 269 | } 270 | 271 | #map { 272 | height:100%; 273 | } 274 | 275 | .chevron { 276 | stroke:#999; 277 | stroke-width:3px; 278 | fill:none; 279 | } 280 | 281 | .fore { 282 | stroke:#999; 283 | stroke-width:1.5px; 284 | } 285 | .back { 286 | fill:#eee; 287 | fill-opacity:.8; 288 | } 289 | .direction { 290 | fill:none; 291 | } 292 | 293 | #home-nav ul { 294 | margin:0; 295 | padding:0; 296 | padding-top:8px; 297 | list-style-type:none; 298 | } 299 | 300 | #home-nav li { 301 | float:right; 302 | margin-right:10px; 303 | } 304 | 305 | #home-nav a:link, #home-nav a:visited { 306 | font-weight:bold; 307 | text-decoration:none; 308 | padding-right:10px; 309 | } 310 | 311 | header h1 a:link, header h1 a:visited { 312 | text-decoration:none; 313 | } 314 | 315 | /* http://sonspring.com/journal/clearing-floats */ 316 | 317 | .clear { 318 | clear: both; 319 | display: block; 320 | overflow: hidden; 321 | visibility: hidden; 322 | width: 0; 323 | height: 0; 324 | } 325 | 326 | /** 327 | * Non-semantic helper classes: please define your styles before this section. 328 | */ 329 | 330 | /* For image replacement */ 331 | .ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; } 332 | 333 | /* Hide for both screenreaders and browsers: 334 | css-discuss.incutio.com/wiki/Screenreader_Visibility */ 335 | .hidden { display: none; visibility: hidden; } 336 | 337 | /* Hide only visually, but have it available for screenreaders: by Jon Neal. 338 | www.webaim.org/techniques/css/invisiblecontent/ & j.mp/visuallyhidden */ 339 | .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } 340 | /* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: drupal.org/node/897638 */ 341 | .visuallyhidden.focusable:active, 342 | .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } 343 | 344 | /* Hide visually and from screenreaders, but maintain layout */ 345 | .invisible { visibility: hidden; } 346 | 347 | /* The Magnificent Clearfix: Updated to prevent margin-collapsing on child elements. 348 | j.mp/bestclearfix */ 349 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } 350 | .clearfix:after { clear: both; } 351 | /* Fix clearfix: blueprintcss.lighthouseapp.com/projects/15318/tickets/5-extra-margin-padding-bottom-of-page */ 352 | .clearfix { zoom: 1; } 353 | 354 | 355 | 356 | /** 357 | * Media queries for responsive design. 358 | * 359 | * These follow after primary styles so they will successfully override. 360 | */ 361 | 362 | @media all and (orientation:portrait) { 363 | /* Style adjustments for portrait mode goes here */ 364 | 365 | } 366 | 367 | @media all and (orientation:landscape) { 368 | /* Style adjustments for landscape mode goes here */ 369 | 370 | } 371 | 372 | /* Grade-A Mobile Browsers (Opera Mobile, Mobile Safari, Android Chrome) 373 | consider this: www.cloudfour.com/css-media-query-for-mobile-is-fools-gold/ */ 374 | @media screen and (max-device-width: 480px) { 375 | 376 | 377 | /* Uncomment if you don't want iOS and WinMobile to mobile-optimize the text for you: j.mp/textsizeadjust */ 378 | /* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */ 379 | } 380 | 381 | 382 | /** 383 | * Print styles. 384 | * 385 | * Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/ 386 | */ 387 | @media print { 388 | * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; 389 | -ms-filter: none !important; } /* Black prints faster: sanbeiji.com/archives/953 */ 390 | a, a:visited { color: #444 !important; text-decoration: underline; } 391 | a[href]:after { content: " (" attr(href) ")"; } 392 | abbr[title]:after { content: " (" attr(title) ")"; } 393 | .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ 394 | pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } 395 | thead { display: table-header-group; } /* css-discuss.incutio.com/wiki/Printing_Tables */ 396 | tr, img { page-break-inside: avoid; } 397 | @page { margin: 0.5cm; } 398 | p, h2, h3 { orphans: 3; widows: 3; } 399 | h2, h3{ page-break-after: avoid; } 400 | } 401 | 402 | -------------------------------------------------------------------------------- /dashboard/static/js/autocomplete.js: -------------------------------------------------------------------------------- 1 | (function(window, $) { 2 | 3 | window._results = {}; 4 | 5 | var search = $('input[type="text"]'), 6 | geocoder = new google.maps.Geocoder(); 7 | 8 | search.autocomplete({ 9 | select: function(event, ui) { 10 | var url = '/search/?', 11 | value = ui.item.value, 12 | parameters = window._results[value]; 13 | parameters['q'] = value; 14 | console.log(parameters); 15 | window.location = url + $.param(parameters); 16 | }, 17 | source: function(request, response) { 18 | // Grab the source objects from Google Maps. 19 | var southwest = new google.maps.LatLng(37.68, -122.57), 20 | northeast = new google.maps.LatLng(37.84, -122.35), 21 | bounds = new google.maps.LatLngBounds(southwest, northeast); 22 | 23 | geocoder.geocode({ 24 | 'address': request.term, 25 | 'bounds': bounds 26 | }, function(results, status){ 27 | response($.map(results, function(item){ 28 | var location = item.geometry.location, 29 | coordinates = { 30 | 'lat': location.lat(), 31 | 'lng': location.lng() 32 | }; 33 | window._results[item.formatted_address] = coordinates; 34 | return { 35 | label: item.formatted_address, 36 | value: item.formatted_address, 37 | } 38 | })); 39 | }); 40 | } 41 | }); 42 | })(window, jQuery); 43 | -------------------------------------------------------------------------------- /dashboard/static/js/charts/dual_state_barchart.js: -------------------------------------------------------------------------------- 1 | var paper; 2 | var barchart = { 3 | clear: function(){ 4 | paper.clear(); 5 | }, 6 | render: function(fromDate,toDate,data) { 7 | function validateDate(d){ 8 | return d.substring(0,2) + "/" + d.substring(2,4) + "/" + d.substring(4,8); 9 | }; 10 | 11 | var totalData = []; 12 | for (i=0; i"); 48 | barsTop.push(barTop); 49 | // Bottom bar 50 | barBottom = paper.rect(origin+(barWidth+spacing)*i,y_orig,barWidth,0); //70 problem with the bar tooltip 51 | barBottom.attr({cursor:"pointer", fill:"#ff0033", opacity:.9, href: "http://www.311dashboard.com/open/" + totalData[i].date, stroke:"none"}); 52 | //cubic-bezier(0.42, 0, 1.0, 1.0) 53 | barBottom.animate({y: y_orig, height: (totalData[i].openCount/10)}, 1000, ">"); 54 | barsBottom.push(barBottom); 55 | } 56 | 57 | var tooltip = paper.rect(10, 10, 110, 30); //draw as a path 58 | /* 59 | //M10 10L110 10L110 30L55 30L60 22L45 30L10 30 60 | //var tooltop = paper.path("M10 10L110 10L110 30L55 30L60 22L45 30L10 30L10 10"); 61 | var tooltop = paper.path("M10 10L110 10L110 30L65 30L60 38L55 30L10 30L10 10"); 62 | tooltop.attr({fill: "#DBDBDB", opacity: .8,stroke:"none"}); 63 | */ 64 | var tooltip_text = paper.text(10,10,""); //draw as a path 65 | tooltip.attr({fill: "#DBDBDB", opacity: .8,stroke:"none"}); 66 | tooltip.hide(); 67 | tooltip_text.hide(); 68 | 69 | var graphWidth = (origin + dayLen*barWidth + (dayLen-1)*spacing); 70 | var index; 71 | var firstDateInData = data[0].date; 72 | var lastDateInData = data[data.length-1].date; 73 | 74 | var date; 75 | var label_text_string; 76 | if (firstDateInData < fromDate){ 77 | date = new Date(fromDate); 78 | var FROM_DATE_IN_MS = fromDate; 79 | } else { 80 | date = new Date(firstDateInData); 81 | var FROM_DATE_IN_MS = firstDateInData; 82 | } 83 | 84 | //alert(date); 85 | /* 86 | var graph_label = paper.text(200,110,label_text_from_string + " to " + validateDate(toDate)); 87 | graph_label.attr({"font-size": 12}); 88 | */ 89 | //var date = new Date(validateDate(fromDate)); //this doesn't work when we don't have data starting with the fromDate 90 | //also doesn't work when there is no data for a day 91 | //var FROM_DATE_IN_MS = date.getTime(); 92 | //var FROM_DATE_IN_MS = fromDate; 93 | //var NUMBER_OF_MS_PER_DAY = 8.64 * 10e7; 94 | var NUMBER_OF_MS_PER_DAY = 86400000; //24*60*60*1000 95 | 96 | barsTop.mouseover(function () { 97 | this.attr({fill:"white",stroke:"grey", opacity:.7, "stroke-width":1, "stroke-linecap":"square"}); 98 | tooltip.show(); 99 | tooltip_text.show(); 100 | //Calculate index for bar text 101 | //graphWidth = (origin + dayLen*barWidth + (dayLen-1)*spacing); //Math.floor?? 357, 28 bars, 28 days 102 | //28 * 10 + (28 - 1)*1 + 50 = 357 103 | index = Math.round(((this.attr('x')/graphWidth) * graphWidth - origin)/(barWidth + spacing)); 104 | //reset the date object 105 | date.setTime(FROM_DATE_IN_MS + index*NUMBER_OF_MS_PER_DAY); //***** 106 | //console.log('index: ' + index); 107 | tooltip.attr({x: this.attr('x')-.5*tooltip.attr('width') + .5*barWidth,y: this.attr('y') - tooltip.attr('height') - 10}); 108 | tooltip_text.attr({text: totalData[index].closedCount + ' Closed Requests\non ' + date.toDateString(), x: this.attr('x') + .5*barWidth, y:this.attr('y') - tooltip.attr('height') + 5}); 109 | }); 110 | 111 | barsBottom.mouseover(function () { 112 | this.attr({fill:"white",stroke:"grey", opacity:.7, "stroke-width":1, "stroke-linecap":"square"}); 113 | tooltip.show(); 114 | tooltip_text.show(); 115 | 116 | index = Math.round(((this.attr('x')/graphWidth) * graphWidth - origin)/(barWidth + spacing)); 117 | 118 | date.setTime(FROM_DATE_IN_MS + index*NUMBER_OF_MS_PER_DAY); 119 | 120 | tooltip.attr({x: this.attr('x')-.5*tooltip.attr('width') + .5*barWidth,y: (this.attr('y')-20) + this.attr('height') + tooltip.attr('height') + 20 - 10}); 121 | tooltip_text.attr({text: totalData[index].openCount + ' Open Requests\non ' + date.toDateString(), x: this.attr('x') + .5*barWidth, y: (this.attr('y')-20) + this.attr('height') + tooltip.attr('height') +20 + 5}); 122 | }); 123 | 124 | barsTop.mouseout(function() { 125 | this.attr({fill:"#1d8dc3", stroke:"none", opacity:.9}); 126 | tooltip.hide(); 127 | tooltip_text.hide(); 128 | }); 129 | 130 | barsBottom.mouseout(function() { 131 | this.attr({fill:"#ff0033", stroke:"none", opacity:.9}); 132 | tooltip.hide(); 133 | tooltip_text.hide(); 134 | }); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /dashboard/static/js/geo_detail.js: -------------------------------------------------------------------------------- 1 | var po = org.polymaps; 2 | 3 | var map = po.map() 4 | .container(document.getElementById("map").appendChild(po.svg("svg"))) 5 | 6 | map.center(centroid) 7 | .zoomRange([12,16]) 8 | .extent(extent); 9 | 10 | map.add(po.image() 11 | .url(po.url("http://{S}tile.cloudmade.com" 12 | + "/b60fbbd51f17456794d2b0e4ed4f0d0c" 13 | + "/998/256/{Z}/{X}/{Y}.png") 14 | .hosts(["a.", "b.", "c.", ""]))); 15 | 16 | map.add(po.image() 17 | .url(po.url("/static/fixed/{Z}/{X}/{Y}.png"))); 18 | 19 | map.add(po.geoJson() 20 | .features([{ "type": "Feature", 21 | "geometry": geometry, 22 | "properties": {}}]) 23 | .id("geometry")); 24 | 25 | var w = 200, 26 | h = 50; 27 | 28 | var x = d3.scale.linear().domain([0, sparkData.length - 1]).range([0, w]); 29 | var y = d3.scale.linear().domain([0, d3.max(sparkData)]).range([h, 0]); 30 | var max = d3.max(sparkData); 31 | var min = d3.min(sparkData); 32 | 33 | var vis = d3.select("#spark-line") 34 | .append("svg:svg") 35 | .attr("width", w + 40) 36 | .attr("height", h + 40) 37 | .append("svg:g") 38 | .attr("transform", "translate(" + 20 + ", " + 20 + ")"); 39 | 40 | var new_circles = vis.selectAll("circle.area") 41 | .data(sparkData) 42 | .enter().append("svg:circle"); 43 | 44 | var line = d3.svg.line() 45 | .x(function(d,i) { return x(i); }) 46 | .y(function(d) { return y(d); }) 47 | .interpolate("cardinal") 48 | 49 | //appending the line 50 | var initial_path = vis.append("svg:path").attr("d", line(sparkData)).attr("stroke-linecap","round"); 51 | 52 | var initial_circles = vis.selectAll("circle.area") 53 | .data(sparkData) 54 | .enter().append("svg:circle") 55 | .attr("class", function(d,i) {if (d === max) { return 'point max'; } else if (d === min) { return 'point' } else { return 'point'}}) 56 | .attr("cx", function(d,i) { return x(i); }) 57 | .attr("cy", function(d,i) { return y(d); }) 58 | .attr("r", function(d) { if (d === max) { return 3.5 } else { return 0}}); 59 | 60 | -------------------------------------------------------------------------------- /dashboard/static/js/libs/chosen.jquery.min.js: -------------------------------------------------------------------------------- 1 | // Chosen, a Select Box Enhancer for jQuery and Protoype 2 | // by Patrick Filler for Harvest, http://getharvest.com 3 | // 4 | // Version 0.9 5 | // Full source at https://github.com/harvesthq/chosen 6 | // Copyright (c) 2011 Harvest http://getharvest.com 7 | 8 | // MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md 9 | // This file is generated by `cake build`, do not edit it by hand. 10 | (function(){var a,b,c,d,e=function(a,b){return function(){return a.apply(b,arguments)}};d=this,a=jQuery,a.fn.extend({chosen:function(c,d){return a(this).each(function(e){if(!a(this).hasClass("chzn-done"))return new b(this,c,d)})}}),b=function(){function b(b){this.set_default_values(),this.form_field=b,this.form_field_jq=a(this.form_field),this.is_multiple=this.form_field.multiple,this.is_rtl=this.form_field_jq.hasClass("chzn-rtl"),this.default_text_default=this.form_field.multiple?"Select Some Options":"Select an Option",this.set_up_html(),this.register_observers(),this.form_field_jq.addClass("chzn-done")}b.prototype.set_default_values=function(){this.click_test_action=e(function(a){return this.test_active_click(a)},this),this.active_field=!1,this.mouse_on_container=!1,this.results_showing=!1,this.result_highlighted=null,this.result_single_selected=null;return this.choices=0},b.prototype.set_up_html=function(){var b,d,e,f;this.container_id=this.form_field.id.length?this.form_field.id.replace(/(:|\.)/g,"_"):this.generate_field_id(),this.container_id+="_chzn",this.f_width=this.form_field_jq.width(),this.default_text=this.form_field_jq.data("placeholder")?this.form_field_jq.data("placeholder"):this.default_text_default,b=a("
",{id:this.container_id,"class":"chzn-container "+(this.is_rtl?" chzn-rtl":void 0),style:"width: "+this.f_width+"px;"}),this.is_multiple?b.html('
    '):b.html(''+this.default_text+'
      '),this.form_field_jq.hide().after(b),this.container=a("#"+this.container_id),this.container.addClass("chzn-container-"+(this.is_multiple?"multi":"single")),this.dropdown=this.container.find("div.chzn-drop").first(),d=this.container.height(),e=this.f_width-c(this.dropdown),this.dropdown.css({width:e+"px",top:d+"px"}),this.search_field=this.container.find("input").first(),this.search_results=this.container.find("ul.chzn-results").first(),this.search_field_scale(),this.search_no_results=this.container.find("li.no-results").first(),this.is_multiple?(this.search_choices=this.container.find("ul.chzn-choices").first(),this.search_container=this.container.find("li.search-field").first()):(this.search_container=this.container.find("div.chzn-search").first(),this.selected_item=this.container.find(".chzn-single").first(),f=e-c(this.search_container)-c(this.search_field),this.search_field.css({width:f+"px"})),this.results_build();return this.set_tab_index()},b.prototype.register_observers=function(){this.container.click(e(function(a){return this.container_click(a)},this)),this.container.mouseenter(e(function(a){return this.mouse_enter(a)},this)),this.container.mouseleave(e(function(a){return this.mouse_leave(a)},this)),this.search_results.click(e(function(a){return this.search_results_click(a)},this)),this.search_results.mouseover(e(function(a){return this.search_results_mouseover(a)},this)),this.search_results.mouseout(e(function(a){return this.search_results_mouseout(a)},this)),this.form_field_jq.bind("liszt:updated",e(function(a){return this.results_update_field(a)},this)),this.search_field.blur(e(function(a){return this.input_blur(a)},this)),this.search_field.keyup(e(function(a){return this.keyup_checker(a)},this)),this.search_field.keydown(e(function(a){return this.keydown_checker(a)},this));if(this.is_multiple){this.search_choices.click(e(function(a){return this.choices_click(a)},this));return this.search_field.focus(e(function(a){return this.input_focus(a)},this))}return this.selected_item.focus(e(function(a){return this.activate_field(a)},this))},b.prototype.container_click=function(b){b&&b.type==="click"&&b.stopPropagation();if(!this.pending_destroy_click){this.active_field?!this.is_multiple&&b&&(a(b.target)===this.selected_item||a(b.target).parents("a.chzn-single").length)&&(b.preventDefault(),this.results_toggle()):(this.is_multiple&&this.search_field.val(""),a(document).click(this.click_test_action),this.results_show());return this.activate_field()}return this.pending_destroy_click=!1},b.prototype.mouse_enter=function(){return this.mouse_on_container=!0},b.prototype.mouse_leave=function(){return this.mouse_on_container=!1},b.prototype.input_focus=function(a){if(!this.active_field)return setTimeout(e(function(){return this.container_click()},this),50)},b.prototype.input_blur=function(a){if(!this.mouse_on_container){this.active_field=!1;return setTimeout(e(function(){return this.blur_test()},this),100)}},b.prototype.blur_test=function(a){if(!this.active_field&&this.container.hasClass("chzn-container-active"))return this.close_field()},b.prototype.close_field=function(){a(document).unbind("click",this.click_test_action),this.is_multiple||(this.selected_item.attr("tabindex",this.search_field.attr("tabindex")),this.search_field.attr("tabindex",-1)),this.active_field=!1,this.results_hide(),this.container.removeClass("chzn-container-active"),this.winnow_results_clear(),this.clear_backstroke(),this.show_search_field_default();return this.search_field_scale()},b.prototype.activate_field=function(){!this.is_multiple&&!this.active_field&&(this.search_field.attr("tabindex",this.selected_item.attr("tabindex")),this.selected_item.attr("tabindex",-1)),this.container.addClass("chzn-container-active"),this.active_field=!0,this.search_field.val(this.search_field.val());return this.search_field.focus()},b.prototype.test_active_click=function(b){return a(b.target).parents("#"+this.container_id).length?this.active_field=!0:this.close_field()},b.prototype.results_build=function(){var a,b,c,e,f,g;c=new Date,this.parsing=!0,this.results_data=d.SelectParser.select_to_array(this.form_field),this.is_multiple&&this.choices>0?(this.search_choices.find("li.search-choice").remove(),this.choices=0):this.is_multiple||this.selected_item.find("span").text(this.default_text),a="",g=this.results_data;for(e=0,f=g.length;e'+a("
      ").text(b.label).html()+""}return""},b.prototype.result_add_option=function(a){var b;if(!a.disabled){a.dom_id=this.container_id+"_o_"+a.array_index,b=a.selected&&this.is_multiple?[]:["active-result"],a.selected&&b.push("result-selected"),a.group_array_index!=null&&b.push("group-option");return'
    • '+a.html+"
    • "}return""},b.prototype.results_update_field=function(){this.result_clear_highlight(),this.result_single_selected=null;return this.results_build()},b.prototype.result_do_highlight=function(a){var b,c,d,e,f;if(a.length){this.result_clear_highlight(),this.result_highlight=a,this.result_highlight.addClass("highlighted"),d=parseInt(this.search_results.css("maxHeight"),10),f=this.search_results.scrollTop(),e=d+f,c=this.result_highlight.position().top+this.search_results.scrollTop(),b=c+this.result_highlight.outerHeight();if(b>=e)return this.search_results.scrollTop(b-d>0?b-d:0);if(c'+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 |
      41 |
      42 |

      43 | 44 |

      45 |
      46 | 47 | 51 |
      52 | 57 |
      58 |
      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 | 38 | 39 | 40 | {% endfor %} 41 |
      {{ request.get_service_name }}{{ request.requested_datetime }}
      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 | 64 | 65 | 66 | {% endfor %} 67 |
      {{ type.service_name }}{{ type.count }}
      68 |
      69 |

      JSON Export

      70 | 71 |
      72 |

      Nearby

      73 | 74 | {% for nearby_place in nearby %} 75 | 76 | 77 | 78 | {% endfor %} 79 |
      {{ nearby_place.name }}{{ nearby_place.street_name|title}}
      80 |
      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 | , let's see how responsive your city is this week. 61 |
      62 |
      63 | Select a map: 64 | 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 |
      12 | {% csrf_token %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
      {{ form.username.label_tag }}{{ form.username }}
      {{ form.password.label_tag }}{{ form.password }}
      23 | 24 | 25 | 26 |
      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 | 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 |
      15 | 16 | 18 | 19 |
      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 |
      1. 9 | {{ street.street_name|title}} 10 | ({{ street.count }} open request{{street.count|pluralize}}) 11 |
      2. 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 | --------------------------------------------------------------------------------