From b3b7bf8513239d7f99d7e7600352df9fc416c53a Mon Sep 17 00:00:00 2001 From: "Jean Cochrane (Lead developer, DataMade)" Date: Wed, 26 Jun 2019 16:25:29 -0500 Subject: [PATCH] Store and load Post.shapes as GeoJSON --- .../management/commands/import_shapes.py | 56 ++++++++++++++++--- .../migrations/0048_post_shape.py | 11 +--- councilmatic_core/models.py | 4 +- councilmatic_core/signals/handlers.py | 3 +- councilmatic_core/templates/partials/map.html | 1 - councilmatic_core/views.py | 4 +- 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/councilmatic_core/management/commands/import_shapes.py b/councilmatic_core/management/commands/import_shapes.py index 1d8c9ffb..56955d79 100644 --- a/councilmatic_core/management/commands/import_shapes.py +++ b/councilmatic_core/management/commands/import_shapes.py @@ -1,6 +1,8 @@ import json -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError +from django.contrib.gis.geos import GEOSGeometry + from councilmatic_core import models @@ -9,12 +11,12 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - 'shape_file', + 'geojson_file', help=( - 'The location of the file containing shapes for each Division, ' - 'relative to the project root. The file should be formatted as JSON ' - 'with each key corresponding to a Division ID and each value corresponding' - 'to a GeoJSON Feature object.' + 'The location of the GeoJSON file containing shapes for each Division, ' + 'relative to the project root. The file should be formatted as a ' + 'GeoJSON FeatureCollection where each Feature A) corresponds to a distinct ' + 'Division and B) has a "division_id" attribute in the "properties" object. ' ) ) @@ -22,12 +24,34 @@ def handle(self, *args, **options): self.stdout.write('Populating shapes for Posts...') shapes_populated = 0 - with open(options['shape_file']) as shapef: + with open(options['geojson_file']) as shapef: shapes = json.load(shapef) - for division_id, shape in shapes.items(): + features = self._get_or_raise( + shapes, + 'features', + 'Could not find the "features" array in the input file.' + ) + + for feature in features: + shape = self._get_or_raise( + feature, + 'geometry', + 'Could not find a "geometry" key in the Feature.' + ) + properties = self._get_or_raise( + feature, + 'properties', + 'Could not find a "properties" key in the Feature.' + ) + division_id = self._get_or_raise( + properties, + 'division_id', + 'Could not find a "division_id" key in the Feature properties.' + ) + models.Post.objects.filter(division_id=division_id).update( - shape=shape + shape=GEOSGeometry(json.dumps(shape)) ) shapes_populated += 1 @@ -36,3 +60,17 @@ def handle(self, *args, **options): 'Populated {} shapes'.format(str(shapes_populated)) ) ) + + def _get_or_raise(self, dct, key, msg): + """ + Check to see if 'dct' has a key corresponding to 'key', and raise an + error if it doesn't. + """ + format_prompt = ( + 'Is the input file formatted as a GeoJSON FeatureCollection ' + 'where each feature has a "division_id" property?' + ) + if not dct.get(key): + raise CommandError(msg + ' ' + format_prompt) + else: + return dct[key] diff --git a/councilmatic_core/migrations/0048_post_shape.py b/councilmatic_core/migrations/0048_post_shape.py index b011531e..af68bb77 100644 --- a/councilmatic_core/migrations/0048_post_shape.py +++ b/councilmatic_core/migrations/0048_post_shape.py @@ -1,6 +1,6 @@ -# Generated by Django 2.1.9 on 2019-06-26 17:10 +# Generated by Django 2.1.9 on 2019-06-26 21:17 -import django.contrib.postgres.fields.jsonb +import django.contrib.gis.db.models.fields import django.core.files.storage from django.db import migrations, models import django.db.models.deletion @@ -17,16 +17,11 @@ class Migration(migrations.Migration): migrations.DeleteModel( name='Post', ), - migrations.AlterField( - model_name='person', - name='headshot', - field=models.FileField(default='images/headshot_placeholder.png', storage=django.core.files.storage.FileSystemStorage(base_url='/', location='/Users/goobzie/datamade/chi-councilmatic'), upload_to='images/headshots'), - ), migrations.CreateModel( name='Post', fields=[ ('post', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='councilmatic_post', serialize=False, to='core.Post')), - ('shape', django.contrib.postgres.fields.jsonb.JSONField()), + ('shape', django.contrib.gis.db.models.fields.GeometryField(null=True, srid=4326)), ], options={ 'abstract': False, diff --git a/councilmatic_core/models.py b/councilmatic_core/models.py index 287b9602..65defd7c 100644 --- a/councilmatic_core/models.py +++ b/councilmatic_core/models.py @@ -2,7 +2,7 @@ import os from django.db import models -from django.contrib.postgres.fields import JSONField +from django.contrib.gis.db import models as geo_models from django.core.exceptions import ImproperlyConfigured from django.conf import settings from django.urls import reverse, NoReverseMatch @@ -247,7 +247,7 @@ class Post(opencivicdata.core.models.Post): on_delete=models.CASCADE, ) - shape = JSONField() + shape = geo_models.GeometryField(null=True) @cached_property def current_member(self): diff --git a/councilmatic_core/signals/handlers.py b/councilmatic_core/signals/handlers.py index d039c890..e312feb7 100644 --- a/councilmatic_core/signals/handlers.py +++ b/councilmatic_core/signals/handlers.py @@ -67,6 +67,5 @@ def create_councilmatic_bill(sender, instance, created, **kwargs): @receiver(post_save, sender=OCDPost) def create_councilmatic_post(sender, instance, created, **kwargs): if created: - cp = CouncilmaticPost(post=instance, - shape={}) + cp = CouncilmaticPost(post=instance) cp.save_base(raw=True) diff --git a/councilmatic_core/templates/partials/map.html b/councilmatic_core/templates/partials/map.html index 69acde50..9a17fbdf 100644 --- a/councilmatic_core/templates/partials/map.html +++ b/councilmatic_core/templates/partials/map.html @@ -75,7 +75,6 @@ }); layer.on('mouseover', function(e){ - console.log(e.target.feature.properties) infoBox.update(e.target.feature.properties); e.target.setStyle({'fillOpacity': 0.6, 'color': "{{MAP_CONFIG.highlight_color}}"}); }); diff --git a/councilmatic_core/views.py b/councilmatic_core/views.py index 766e2f6f..adcc21a0 100644 --- a/councilmatic_core/views.py +++ b/councilmatic_core/views.py @@ -195,7 +195,7 @@ def map(self): feature = { 'type': 'Feature', - 'geometry': post.shape, + 'geometry': json.loads(post.shape.json), 'properties': { 'district': post.label, 'council_member': council_member, @@ -379,7 +379,7 @@ def get_context_data(self, **kwargs): feature = { 'type': 'Feature', - 'geometry': person.latest_council_membership.post.shape, + 'geometry': json.loads(person.latest_council_membership.post.shape.json), 'properties': { 'district': person.latest_council_membership.post.label, }