Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import of newly refactored app.

  • Loading branch information...
commit b5512a222fcf48a1d615b97e8da3b024d7f1d341 0 parents
@onyxfish onyxfish authored
Showing with 781 additions and 0 deletions.
  1. +6 −0 AUTHORS
  2. +21 −0 COPYING
  3. +41 −0 README.textile
  4. 0  boundaryservice/__init__.py
  5. BIN  boundaryservice/__init__.pyc
  6. +27 −0 boundaryservice/admin.py
  7. BIN  boundaryservice/admin.pyc
  8. +30 −0 boundaryservice/authentication.py
  9. BIN  boundaryservice/authentication.pyc
  10. +97 −0 boundaryservice/fields.py
  11. BIN  boundaryservice/fields.pyc
  12. 0  boundaryservice/management/__init__.py
  13. BIN  boundaryservice/management/__init__.pyc
  14. BIN  boundaryservice/management/commands/.load_shapefiles.py.swp
  15. 0  boundaryservice/management/commands/__init__.py
  16. BIN  boundaryservice/management/commands/__init__.pyc
  17. +171 −0 boundaryservice/management/commands/load_shapefiles.py
  18. BIN  boundaryservice/management/commands/load_shapefiles.pyc
  19. +133 −0 boundaryservice/models.py
  20. BIN  boundaryservice/models.pyc
  21. +72 −0 boundaryservice/resources.py
  22. BIN  boundaryservice/resources.pyc
  23. +106 −0 boundaryservice/tastyhacks.py
  24. BIN  boundaryservice/tastyhacks.pyc
  25. +11 −0 boundaryservice/throttle.py
  26. BIN  boundaryservice/throttle.pyc
  27. +12 −0 boundaryservice/urls.py
  28. BIN  boundaryservice/urls.pyc
  29. +10 −0 boundaryservice/utils.py
  30. BIN  boundaryservice/utils.pyc
  31. +22 −0 boundaryservice/views.py
  32. +22 −0 setup.py
6 AUTHORS
@@ -0,0 +1,6 @@
+Christopher Groskopf (@onyxfish)
+Ryan Nagle (@ryannagle)
+Ryan Mark (@ryanmark)
+Joe Germuska (@joegermuska)
+Brian Boyer (@brianboyer)
+Anders Eriksen (@anderseri)
21 COPYING
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2011 Chicago Tribune, Christopher Groskopf, Ryan Nagle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
41 README.textile
@@ -0,0 +1,41 @@
+h1. The Newsapps Boundary Service
+
+The Boundary Service is a ready-to-deploy system for aggregating regional boundary data (from shapefiles) and republishing that data via a RESTful JSON API. It is packaged as a pluggable Django application so that it can be easily integrated into another project, or it may also be used with the prepackaged <a href="https://github.com/newsapps/boundaryservice-demo">boundaryservice-demo</a>.
+
+This project is aimed at providing a simple service for newsrooms, open-government hackers and others to centralize and build on regional GIS data. You can see the instance we've configured for Chicago & Illinois, along with much more detailed information about the API at <a href="http://boundaries.tribapps.com/">http://boundaries.tribapps.com/</a>.
+
+h2. Installation
+
+<pre>
+pip install django-boundaryservice
+</pre>
+
+h2. Using the shapefile loader
+
+By default the shapefile loader will expect you to have created the path "data/shapefiles" relative to your manage.py script. Shapefiles and your definitions.py go into this folder. See <a href="https://github.com/newsapps/boundaryservice-demo">boundaryservice-demo</a> for a complete example. You may also override the default location by passing the "-d" flag to the command:
+
+<pre>python manage.py load_shapefiles -d data_dir</pre>
+
+h2. Adding data
+
+To add data to the Boundary Service you will first need to add a shapefile and its related files (prj, dbf, etc.) to the data/shapefiles directory. See data/shapefiles/neighborhoods for an example shapefile.
+
+Once your data is in place, you will modify data/shapefiles/definitions.py to add a declaration for your new shapefile to the SHAPEFILES dictionary. The Chicago neighborhoods example includes extensive commenting describing the various fields and how they should be populated. Note that the Boundary Service will normally be able to infer the projection of your shapefile and automatically transform it to an appropriate internal representation.
+
+Of particular note amongst the fields are the 'ider' and 'namer' properties. These should be assigned to functions which will be passed a feature's attributes as a dictionary. 'ider' should return a unique external id for the feature. (e.g. a district id number, geographic id code or any sequential primary key) Whenever possible these ids should be stable across revisions to the dataset. 'namer' should return a canonical name for the feature, not including its kind. (e.g. "Austin" for the Austin Community Area, "Chicago" for the City of Chicago, or #42 for Police Beat #42) A number of callable classes are defined in data/shapefiles/utils.py, which should mitigate the need to write custom functions for each dataset.
+
+Once definitions.py has been saved the new shapefile can be loaded by running:
+
+<pre>./manage load_shapefiles -o BoundaryKindWithoutWhitespace</pre>
+
+The "-c" parameter can also be passed to clear existing boundaries of only the specified type and then load the data. Multiple boundaries can be cleared and loaded by passing a comma-separated list to "-o".
+
+As a matter of best practice when shapefiles have been acquired from government entities and other primary sources it is advisable not to modify them before loading them into the Boundary Service. (Thus why the Chicago neighborhoods shapefile is misspelled "Neighboorhoods".) If it is necessary to modify the data this should be noted in the 'notes' field of the shapefile's definitions.py entry.
+
+h2. Credits
+
+The Boundary Service is a product of the <a href="http://blog.apps.chicagotribune.com">News Applications team</a> at the Chicago Tribune. Core development was done by <a href="http://twitter.com/onyxfish">Christopher Groskopf</a> and <a href="http://twitter.com/ryannagle">Ryan Nagle</a>.
+
+h2. License
+
+MIT.
0  boundaryservice/__init__.py
No changes.
BIN  boundaryservice/__init__.pyc
Binary file not shown
27 boundaryservice/admin.py
@@ -0,0 +1,27 @@
+from django.contrib import admin
+from django.contrib.gis.admin import OSMGeoAdmin
+from tastypie.models import ApiAccess, ApiKey
+
+from boundaryservice.models import BoundarySet, Boundary
+
+class ApiAccessAdmin(admin.ModelAdmin):
+ pass
+
+admin.site.register(ApiAccess, ApiAccessAdmin)
+
+class ApiKeyAdmin(admin.ModelAdmin):
+ pass
+
+admin.site.register(ApiKey, ApiKeyAdmin)
+
+class BoundarySetAdmin(admin.ModelAdmin):
+ list_filter = ('authority', 'domain')
+
+admin.site.register(BoundarySet, BoundarySetAdmin)
+
+class BoundaryAdmin(OSMGeoAdmin):
+ list_display = ('kind', 'name', 'external_id')
+ list_display_links = ('name', 'external_id')
+ list_filter = ('kind',)
+
+admin.site.register(Boundary, BoundaryAdmin)
BIN  boundaryservice/admin.pyc
Binary file not shown
30 boundaryservice/authentication.py
@@ -0,0 +1,30 @@
+from django.contrib.auth.models import User
+
+from tastypie.authentication import ApiKeyAuthentication
+
+class NoOpApiKeyAuthentication(ApiKeyAuthentication):
+ """
+ Allows all users access to all objects, but ensures ApiKeys are properly processed for throttling.
+ """
+ def is_authenticated(self, request, **kwargs):
+
+ username = request.GET.get('username') or request.POST.get('username')
+ api_key = request.GET.get('api_key') or request.POST.get('api_key')
+
+ if not username:
+ return True
+
+ try:
+ user = User.objects.get(username=username)
+ except (User.DoesNotExist, User.MultipleObjectsReturned):
+ return self._unauthorized()
+
+ request.user = user
+
+ return self.get_key(user, api_key)
+
+ def _get_anonymous_identifier(self, request):
+ return 'anonymous_%s' % request.META.get('REMOTE_ADDR', 'noaddr')
+
+ def get_identifier(self, request):
+ return request.REQUEST.get('username', self._get_anonymous_identifier(request))
BIN  boundaryservice/authentication.pyc
Binary file not shown
97 boundaryservice/fields.py
@@ -0,0 +1,97 @@
+"""
+Custom model fields.
+"""
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db import models
+from django.utils import simplejson as json
+
+
+class ListField(models.TextField):
+ """
+ Store a list of values in a Model field.
+ """
+ __metaclass__ = models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ self.separator = kwargs.pop('separator', ',')
+ super(ListField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ if not value: return
+
+ if isinstance(value, list):
+ return value
+
+ return value.split(self.separator)
+
+ def get_prep_value(self, value):
+ if not value: return
+
+ if not isinstance(value, list) and not isinstance(value, tuple):
+ raise ValueError('Value for ListField must be either a list or tuple.')
+
+ return self.separator.join([unicode(s) for s in value])
+
+ def value_to_string(self, obj):
+ value = self._get_val_from_obj(obj)
+
+ return self.get_prep_value(value)
+
+class JSONField(models.TextField):
+ """
+ Store arbitrary JSON in a Model field.
+ """
+ # Used so to_python() is called
+ __metaclass__ = models.SubfieldBase
+
+ def to_python(self, value):
+ """
+ Convert string value to JSON after its loaded from the database.
+ """
+ if value == "":
+ return None
+
+ try:
+ if isinstance(value, basestring):
+ return json.loads(value)
+ except ValueError:
+ pass
+
+ return value
+
+ def get_prep_value(self, value):
+ """
+ Convert our JSON object to a string before being saved.
+ """
+ if value == "":
+ return None
+
+ if isinstance(value, dict) or isinstance(value, list):
+ value = json.dumps(value, cls=DjangoJSONEncoder)
+
+ return super(JSONField, self).get_prep_value(value)
+
+ def value_to_string(self, obj):
+ """
+ Called by the serializer.
+ """
+ value = self._get_val_from_obj(obj)
+
+ return self.get_db_prep_value(value)
+
+try:
+ from south.modelsinspector import add_introspection_rules
+
+ add_introspection_rules([], ["^boundaries\.lib\.fields\.JSONField"])
+
+ add_introspection_rules([
+ (
+ [ListField],
+ [],
+ {
+ "separator": ["separator", {"default": ","}],
+ },
+ ),
+ ], ["^boundaries\.lib\.fields\.ListField"])
+except ImportError:
+ pass
BIN  boundaryservice/fields.pyc
Binary file not shown
0  boundaryservice/management/__init__.py
No changes.
BIN  boundaryservice/management/__init__.pyc
Binary file not shown
BIN  boundaryservice/management/commands/.load_shapefiles.py.swp
Binary file not shown
0  boundaryservice/management/commands/__init__.py
No changes.
BIN  boundaryservice/management/commands/__init__.pyc
Binary file not shown
171 boundaryservice/management/commands/load_shapefiles.py
@@ -0,0 +1,171 @@
+import logging
+log = logging.getLogger('boundaries.api.load_shapefiles')
+from optparse import make_option
+import os
+import sys
+
+from django.conf import settings
+from django.contrib.gis.gdal import CoordTransform, DataSource, OGRGeometry, OGRGeomType
+from django.core.management.base import BaseCommand
+from django.db import connections, DEFAULT_DB_ALIAS
+
+from boundaryservice.models import BoundarySet, Boundary
+
+DEFAULT_SHAPEFILES_DIR = getattr(settings, 'SHAPEFILES_DIR', 'data/shapefiles')
+GEOMETRY_COLUMN = 'shape'
+
+class Command(BaseCommand):
+ help = 'Import boundaries described by shapefiles.'
+ option_list = BaseCommand.option_list + (
+ make_option('-c', '--clear', action='store_true', dest='clear',
+ help='Clear all jurisdictions in the DB.'),
+ make_option('-d', '--data-dir', action='store', dest='data_dir',
+ default=DEFAULT_SHAPEFILES_DIR,
+ help='Load shapefiles from this directory'),
+ make_option('-e', '--except', action='store', dest='except',
+ help='Don\'t load these kinds of Areas, comma-delimitted.'),
+ make_option('-o', '--only', action='store', dest='only',
+ help='Only load these kinds of Areas, comma-delimitted.'),
+ )
+
+ def get_version(self):
+ return '0.1'
+
+ def handle(self, *args, **options):
+ # Load configuration
+ sys.path.append(options['data_dir'])
+ from definitions import SHAPEFILES
+
+ if options['only']:
+ only = options['only'].split(',')
+ # TODO: stripping whitespace here because optparse doesn't handle it correclty
+ sources = [s for s in SHAPEFILES if s.replace(' ', '') in only]
+ elif options['except']:
+ exceptions = options['except'].upper().split(',')
+ # See above
+ sources = [s for s in SHAPEFILES if s.replace(' ', '') not in exceptions]
+ else:
+ sources = [s for s in SHAPEFILES]
+
+ # Get spatial reference system for the postgis geometry field
+ geometry_field = Boundary._meta.get_field_by_name(GEOMETRY_COLUMN)[0]
+ SpatialRefSys = connections[DEFAULT_DB_ALIAS].ops.spatial_ref_sys()
+ db_srs = SpatialRefSys.objects.get(srid=geometry_field.srid).srs
+
+ for kind, config in SHAPEFILES.items():
+ if kind not in sources:
+ log.info('Skipping %s.' % kind)
+ continue
+
+ log.info('Processing %s.' % kind)
+
+ if options['clear']:
+ set = None
+
+ try:
+ set = BoundarySet.objects.get(name=kind)
+ except BoundarySet.DoesNotExist:
+ pass
+
+ if set:
+ log.info('Clearing old %s.' % kind)
+ set.boundaries.all().delete()
+ set.delete()
+ log.info('Loading new %s.' % kind)
+
+ path = os.path.join(options['data_dir'], config['file'])
+ datasource = DataSource(path)
+
+ # Assume only a single-layer in shapefile
+ if datasource.layer_count > 1:
+ log.warn('%s shapefile has multiple layers, using first.' % kind)
+
+ layer = datasource[0]
+ if 'srid' in config and config['srid']:
+ layer_srs = SpatialRefSys.objects.get(srid=config['srid']).srs
+ else:
+ layer_srs = layer.srs
+
+ # Create a convertor to turn the source data into
+ transformer = CoordTransform(layer_srs, db_srs)
+
+ # Create BoundarySet
+ set = BoundarySet.objects.create(
+ name=kind,
+ singular=config['singular'],
+ kind_first=config['kind_first'],
+ authority=config['authority'],
+ domain=config['domain'],
+ last_updated=config['last_updated'],
+ href=config['href'],
+ notes=config['notes'],
+ count=len(layer),
+ metadata_fields=layer.fields)
+
+ for feature in layer:
+ # Transform the geometry to the correct SRS
+ geometry = self.polygon_to_multipolygon(feature.geom)
+ geometry.transform(transformer)
+
+ # Create simplified geometry field by collapsing points within 1/1000th of a degree.
+ # Since Chicago is at approx. 42 degrees latitude this works out to an margin of
+ # roughly 80 meters latitude and 112 meters longitude.
+ # Preserve topology prevents a shape from ever crossing over itself.
+ simple_geometry = geometry.geos.simplify(0.0001, preserve_topology=True)
+
+ # Conversion may force multipolygons back to being polygons
+ simple_geometry = self.polygon_to_multipolygon(simple_geometry.ogr)
+
+ # Extract metadata into a dictionary
+ metadata = {}
+
+ for field in layer.fields:
+
+ # Decode string fields using encoding specified in definitions config
+ if config['encoding'] != '':
+ try:
+ metadata[field] = feature.get(field).decode(config['encoding'])
+ # Only strings will be decoded, get value in normal way if int etc.
+ except AttributeError:
+ metadata[field] = feature.get(field)
+ else:
+ metadata[field] = feature.get(field)
+
+ external_id = config['ider'](feature)
+ feature_name = config['namer'](feature)
+
+ # If encoding is specified, decode id and feature name
+ if config['encoding'] != '':
+ external_id = external_id.decode(config['encoding'])
+ feature_name = feature_name.decode(config['encoding'])
+
+ if config['kind_first']:
+ display_name = '%s %s' % (config['singular'], feature_name)
+ else:
+ display_name = '%s %s' % (feature_name, config['singular'])
+
+ Boundary.objects.create(
+ set=set,
+ kind=config['singular'],
+ external_id=external_id,
+ name=feature_name,
+ display_name=display_name,
+ metadata=metadata,
+ shape=geometry.wkt,
+ simple_shape=simple_geometry.wkt,
+ centroid=geometry.geos.centroid)
+
+ log.info('Saved %i %s.' % (set.count, kind))
+
+ def polygon_to_multipolygon(self, geom):
+ """
+ Convert polygons to multipolygons so all features are homogenous in the database.
+ """
+ if geom.__class__.__name__ == 'Polygon':
+ g = OGRGeometry(OGRGeomType('MultiPolygon'))
+ g.add(geom)
+ return g
+ elif geom.__class__.__name__ == 'MultiPolygon':
+ return geom
+ else:
+ raise ValueError('Geom is neither Polygon nor MultiPolygon.')
BIN  boundaryservice/management/commands/load_shapefiles.pyc
Binary file not shown
133 boundaryservice/models.py
@@ -0,0 +1,133 @@
+from django.conf import settings
+from django.contrib.gis.db import models
+
+from boundaryservice.fields import ListField, JSONField
+from boundaryservice.utils import get_site_url_root
+
+def get_site_url_root():
+ domain = getattr(settings, 'MY_SITE_DOMAIN', 'localhost')
+ protocol = getattr(settings, 'MY_SITE_PROTOCOL', 'http')
+ port = getattr(settings, 'MY_SITE_PORT', '')
+ url = '%s://%s' % (protocol, domain)
+ if port:
+ url += ':%s' % port
+ return url
+
+class SluggedModel(models.Model):
+ """
+ Extend this class to get a slug field and slug generated from a model
+ field. We call the 'get_slug_text', '__unicode__' or '__str__'
+ methods (in that order) on save() to get text to slugify. The slug may
+ have numbers appended to make sure the slug is unique.
+ """
+ slug = models.SlugField(max_length=256)
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+ self.unique_slug()
+ if self.slug == '': raise ValueError, "Slug may not be blank [%s]" % str(self)
+ super(SluggedModel,self).save(*args, **kwargs)
+
+ def unique_slug(self):
+ """
+ Customized unique_slug function
+ """
+ if not getattr(self, "slug"): # if it's already got a slug, do nothing.
+ from django.template.defaultfilters import slugify
+ if hasattr(self,'get_slug_text') and callable(self.get_slug_text):
+ slug_txt = self.get_slug_text()
+ elif hasattr(self,'__unicode__'):
+ slug_txt = unicode(self)
+ elif hasattr(self,'__str__'):
+ slug_txt = str(self)
+ else:
+ return
+ slug = slugify(slug_txt)
+
+ itemModel = self.__class__
+ # the following gets all existing slug values
+ allSlugs = set(sl.values()[0] for sl in itemModel.objects.values("slug"))
+ if slug in allSlugs:
+ counterFinder = re.compile(r'-\d+$')
+ counter = 2
+ slug = "%s-%i" % (slug, counter)
+ while slug in allSlugs:
+ slug = re.sub(counterFinder,"-%i" % counter, slug)
+ counter += 1
+
+ setattr(self,"slug",slug)
+
+ def fully_qualified_url(self):
+ return get_site_url_root() + self.get_absolute_url()
+
+class BoundarySet(SluggedModel):
+ """
+ A set of related boundaries, such as all Wards or Neighborhoods.
+ """
+ name = models.CharField(max_length=64, unique=True,
+ help_text='Category of boundaries, e.g. "Community Areas".')
+ singular = models.CharField(max_length=64,
+ help_text='Name of a single boundary, e.g. "Community Area".')
+ kind_first = models.BooleanField(
+ help_text='If true, boundary display names will be "kind name" (e.g. Austin Community Area), otherwise "name kind" (e.g. 43rd Precinct).')
+ authority = models.CharField(max_length=256,
+ help_text='The entity responsible for this data\'s accuracy, e.g. "City of Chicago".')
+ domain = models.CharField(max_length=256,
+ help_text='The area that this BoundarySet covers, e.g. "Chicago" or "Illinois".')
+ last_updated = models.DateField(
+ help_text='The last time this data was updated from its authority (but not necessarily the date it is current as of).')
+ href = models.URLField(blank=True,
+ help_text='The url this data was found at, if any.')
+ notes = models.TextField(blank=True,
+ help_text='Notes about loading this data, including any transformations that were applied to it.')
+ count = models.IntegerField(
+ help_text='Total number of features in this boundary set.')
+ metadata_fields = ListField(separator='|', blank=True,
+ help_text='What, if any, metadata fields were loaded from the original dataset.')
+
+ class Meta:
+ ordering = ('name',)
+
+ def __unicode__(self):
+ """
+ Print plural names.
+ """
+ return unicode(self.name)
+
+class Boundary(SluggedModel):
+ """
+ A boundary object, such as a Ward or Neighborhood.
+ """
+ set = models.ForeignKey(BoundarySet, related_name='boundaries',
+ help_text='Category of boundaries that this boundary belongs, e.g. "Community Areas".')
+ kind = models.CharField(max_length=64,
+ help_text='A copy of BoundarySet\'s "singular" value for purposes of slugging and inspection.')
+ external_id = models.CharField(max_length=64,
+ help_text='The boundaries\' unique id in the source dataset, or a generated one.')
+ name = models.CharField(max_length=192, db_index=True,
+ help_text='The name of this boundary, e.g. "Austin".')
+ display_name = models.CharField(max_length=256,
+ help_text='The name and kind of the field to be used for display purposes.')
+ metadata = JSONField(blank=True,
+ help_text='The complete contents of the attribute table for this boundary from the source shapefile, structured as json.')
+ shape = models.MultiPolygonField(srid=4269,
+ help_text='The geometry of this boundary in EPSG:4269 projection.')
+ simple_shape = models.MultiPolygonField(srid=4269,
+ help_text='The geometry of this boundary in EPSG:4269 projection and simplified to 0.0001 tolerance.')
+ centroid = models.PointField(srid=4269,
+ null=True,
+ help_text='The centroid (weighted center) of this boundary in EPSG:4269 projection.')
+
+ objects = models.GeoManager()
+
+ class Meta:
+ ordering = ('kind', 'display_name')
+
+ def __unicode__(self):
+ """
+ Print names are formatted like "Austin Community Area"
+ and will slug like "austin-community-area".
+ """
+ return unicode(self.display_name)
BIN  boundaryservice/models.pyc
Binary file not shown
72 boundaryservice/resources.py
@@ -0,0 +1,72 @@
+import re
+
+from django.contrib.gis.measure import D
+from tastypie import fields
+from tastypie.serializers import Serializer
+
+from boundaryservice.authentication import NoOpApiKeyAuthentication
+from boundaryservice.models import BoundarySet, Boundary
+from boundaryservice.tastyhacks import SluggedResource
+from boundaryservice.throttle import AnonymousThrottle
+
+class BoundarySetResource(SluggedResource):
+ boundaries = fields.ToManyField('boundaries.resources.BoundaryResource', 'boundaries')
+
+ class Meta:
+ queryset = BoundarySet.objects.all()
+ serializer = Serializer(formats=['json', 'jsonp'], content_types = {'json': 'application/json', 'jsonp': 'text/javascript'})
+ resource_name = 'boundary-set'
+ excludes = ['id', 'singular', 'kind_first']
+ allowed_methods = ['get']
+ authentication = NoOpApiKeyAuthentication()
+ #throttle = AnonymousThrottle(throttle_at=100)
+
+class BoundaryResource(SluggedResource):
+ set = fields.ForeignKey(BoundarySetResource, 'set')
+
+ class Meta:
+ queryset = Boundary.objects.all()
+ serializer = Serializer(formats=['json', 'jsonp'], content_types = {'json': 'application/json', 'jsonp': 'text/javascript'})
+ resource_name = 'boundary'
+ excludes = ['id', 'display_name', 'shape']
+ allowed_methods = ['get']
+ authentication = NoOpApiKeyAuthentication()
+ #throttle = AnonymousThrottle(throttle_at=100)
+
+ def build_filters(self, filters=None):
+ """
+ Override build_filters to support geoqueries.
+ """
+ if filters is None:
+ filters = {}
+
+ orm_filters = super(BoundaryResource, self).build_filters(filters)
+
+ if 'sets' in filters:
+ sets = filters['sets'].split(',')
+
+ orm_filters.update({'set__slug__in': sets})
+
+ if 'contains' in filters:
+ lat, lon = filters['contains'].split(',')
+ wkt_pt = 'POINT(%s %s)' % (lon, lat)
+
+ orm_filters.update({'shape__contains': wkt_pt})
+
+ if 'near' in filters:
+ lat, lon, range = filters['near'].split(',')
+ wkt_pt = 'POINT(%s %s)' % (lon, lat)
+ numeral = re.match('([0-9]+)', range).group(1)
+ unit = range[len(numeral):]
+ numeral = int(numeral)
+ kwargs = {unit: numeral}
+
+ orm_filters.update({'shape__distance_lte': (wkt_pt, D(**kwargs))})
+
+ if 'intersects' in filters:
+ slug = filters['intersects']
+ bounds = Boundary.objects.get(slug=slug)
+
+ orm_filters.update({'shape__intersects': bounds.shape})
+
+ return orm_filters
BIN  boundaryservice/resources.pyc
Binary file not shown
106 boundaryservice/tastyhacks.py
@@ -0,0 +1,106 @@
+from django.conf.urls.defaults import url
+from django.contrib.gis.db.models import GeometryField
+from django.utils import simplejson
+
+from tastypie.bundle import Bundle
+from tastypie.fields import ApiField, CharField
+from tastypie.resources import ModelResource
+from tastypie.utils import trailing_slash
+
+from boundaryservice.fields import ListField, JSONField
+
+class ListApiField(ApiField):
+ """
+ Custom ApiField for dealing with data from custom ListFields.
+ """
+ dehydrated_type = 'list'
+ help_text = 'Delimited list of items.'
+
+ def dehydrate(self, obj):
+ return self.convert(super(ListApiField, self).dehydrate(obj))
+
+ def convert(self, value):
+ if value is None:
+ return None
+
+ return value
+
+class JSONApiField(ApiField):
+ """
+ Custom ApiField for dealing with data from custom JSONFields.
+ """
+ dehydrated_type = 'json'
+ help_text = 'JSON structured data.'
+
+ def dehydrate(self, obj):
+ return self.convert(super(JSONApiField, self).dehydrate(obj))
+
+ def convert(self, value):
+ if value is None:
+ return None
+
+ return value
+
+class GeometryApiField(ApiField):
+ """
+ Custom ApiField for dealing with data from GeometryFields (by serializing them as GeoJSON) .
+ """
+ dehydrated_type = 'geometry'
+ help_text = 'Geometry data.'
+
+ def dehydrate(self, obj):
+ return self.convert(super(GeometryApiField, self).dehydrate(obj))
+
+ def convert(self, value):
+ if value is None:
+ return None
+
+ # Get ready-made geojson serialization and then convert it _back_ to a Python object
+ # so that Tastypie can serialize it as part of the bundle
+ return simplejson.loads(value.geojson)
+
+
+class SluggedResource(ModelResource):
+ """
+ ModelResource subclass that handles looking up models by slugs rather than IDs.
+ """
+ def override_urls(self):
+ """
+ Add slug-based url pattern.
+ """
+ return [
+ url(r"^(?P<resource_name>%s)/schema%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_schema'), name="api_get_schema"),
+ url(r"^(?P<resource_name>%s)/(?P<slug>[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
+ ]
+
+ def get_resource_uri(self, bundle_or_obj):
+ """
+ Override URI generation to use slugs.
+ """
+ kwargs = {
+ 'resource_name': self._meta.resource_name,
+ }
+
+ if isinstance(bundle_or_obj, Bundle):
+ kwargs['slug'] = bundle_or_obj.obj.slug
+ else:
+ kwargs['slug'] = bundle_or_obj.slug
+
+ if self._meta.api_name is not None:
+ kwargs['api_name'] = self._meta.api_name
+
+ return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
+
+ @classmethod
+ def api_field_from_django_field(cls, f, default=CharField):
+ """
+ Overrides default field handling to support custom ListField and JSONField.
+ """
+ if isinstance(f, ListField):
+ return ListApiField
+ elif isinstance(f, JSONField):
+ return JSONApiField
+ elif isinstance(f, GeometryField):
+ return GeometryApiField
+
+ return super(SluggedResource, cls).api_field_from_django_field(f, default)
BIN  boundaryservice/tastyhacks.pyc
Binary file not shown
11 boundaryservice/throttle.py
@@ -0,0 +1,11 @@
+from tastypie.throttle import CacheThrottle
+
+class AnonymousThrottle(CacheThrottle):
+ """
+ Anonymous users are throttled, but those with a valid API key are not.
+ """
+ def should_be_throttled(self, identifier, **kwargs):
+ if not identifier.startswith('anonymous_'):
+ return False
+
+ return super(AnonymousThrottle, self).should_be_throttled(identifier, **kwargs)
BIN  boundaryservice/throttle.pyc
Binary file not shown
12 boundaryservice/urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls.defaults import patterns, include
+from tastypie.api import Api
+
+from boundaryservice.resources import BoundarySetResource, BoundaryResource
+
+v1_api = Api(api_name='1.0')
+v1_api.register(BoundarySetResource())
+v1_api.register(BoundaryResource())
+
+urlpatterns = patterns('',
+ (r'', include(v1_api.urls)),
+)
BIN  boundaryservice/urls.pyc
Binary file not shown
10 boundaryservice/utils.py
@@ -0,0 +1,10 @@
+from django.conf import settings
+
+def get_site_url_root():
+ domain = getattr(settings, 'MY_SITE_DOMAIN', 'localhost')
+ protocol = getattr(settings, 'MY_SITE_PROTOCOL', 'http')
+ port = getattr(settings, 'MY_SITE_PORT', '')
+ url = '%s://%s' % (protocol, domain)
+ if port:
+ url += ':%s' % port
+ return url
BIN  boundaryservice/utils.pyc
Binary file not shown
22 boundaryservice/views.py
@@ -0,0 +1,22 @@
+from django.core.urlresolvers import reverse, resolve
+from django.http import Http404
+from django.shortcuts import get_object_or_404
+
+from boundaryservice.models import Boundary
+
+def external_id_redirects(request, api_name, resource_name, slug, external_id):
+ """
+ Fake-redirects /boundary-set/slug/external_id paths to the proper canonical boundary path.
+ """
+ if resource_name != 'boundary-set':
+ raise Http404
+
+ boundary = get_object_or_404(Boundary, set__slug=slug, external_id=external_id)
+
+ # This bit of hacky code allows to execute the resource view as the canonical url were hit, but without redirecting
+ # Note that the resource will still have correct, canonical 'resource_uri' attribute attached
+ canonical_url = reverse('api_dispatch_detail', kwargs={'api_name': api_name, 'resource_name': 'boundary', 'slug': boundary.slug})
+ view, args, kwargs = resolve(canonical_url)
+
+ return view(request, *args, **kwargs)
+
22 setup.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(
+ name = "django-boundaryservice",
+ version = "0.1.0",
+ description = "A reusable system for aggregating and providing API access to regional boundary data.",
+ long_description = open('README.textile').read(),
+ author='Christopher Groskopf',
+ author_email='staringmonkey@gmail.com',
+ url='http://blog.apps.chicagotribune.com/',
+ license = "MIT",
+ packages = [
+ 'boundaryservice',
+ 'boundaryservice.management',
+ 'boundaryservice.management.commands'
+ ],
+ install_requires = [
+ 'django-tastypie'
+ ]
+)
Please sign in to comment.
Something went wrong with that request. Please try again.