diff --git a/councilmatic_core/management/commands/import_shapes.py b/councilmatic_core/management/commands/import_shapes.py new file mode 100644 index 00000000..56955d79 --- /dev/null +++ b/councilmatic_core/management/commands/import_shapes.py @@ -0,0 +1,76 @@ +import json + +from django.core.management.base import BaseCommand, CommandError +from django.contrib.gis.geos import GEOSGeometry + +from councilmatic_core import models + + +class Command(BaseCommand): + help = "Import boundary shapefiles for Post entities" + + def add_arguments(self, parser): + parser.add_argument( + 'geojson_file', + help=( + '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. ' + ) + ) + + def handle(self, *args, **options): + self.stdout.write('Populating shapes for Posts...') + shapes_populated = 0 + + with open(options['geojson_file']) as shapef: + shapes = json.load(shapef) + + 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=GEOSGeometry(json.dumps(shape)) + ) + shapes_populated += 1 + + self.stdout.write( + self.style.SUCCESS( + '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 new file mode 100644 index 00000000..af68bb77 --- /dev/null +++ b/councilmatic_core/migrations/0048_post_shape.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.9 on 2019-06-26 21:17 + +import django.contrib.gis.db.models.fields +import django.core.files.storage +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20171005_2028'), + ('councilmatic_core', '0047_update_filepath'), + ] + + operations = [ + migrations.DeleteModel( + name='Post', + ), + 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.gis.db.models.fields.GeometryField(null=True, srid=4326)), + ], + options={ + 'abstract': False, + }, + bases=('core.post',), + ), + ] diff --git a/councilmatic_core/models.py b/councilmatic_core/models.py index f6149716..65defd7c 100644 --- a/councilmatic_core/models.py +++ b/councilmatic_core/models.py @@ -2,6 +2,7 @@ import os from django.db import models +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 @@ -233,8 +234,11 @@ def link_html(self): class Post(opencivicdata.core.models.Post): - class Meta: - proxy = True + + post = models.OneToOneField(opencivicdata.core.models.Post, + on_delete=models.CASCADE, + related_name='councilmatic_post', + parent_link=True) organization = ProxyForeignKey( Organization, @@ -243,6 +247,8 @@ class Meta: on_delete=models.CASCADE, ) + shape = geo_models.GeometryField(null=True) + @cached_property def current_member(self): membership = self.memberships.filter(end_date_dt__gt=timezone.now())\ diff --git a/councilmatic_core/signals/handlers.py b/councilmatic_core/signals/handlers.py index b5d81441..e312feb7 100644 --- a/councilmatic_core/signals/handlers.py +++ b/councilmatic_core/signals/handlers.py @@ -3,14 +3,16 @@ from django.utils.text import slugify, Truncator from opencivicdata.core.models import (Organization as OCDOrganization, - Person as OCDPerson) + Person as OCDPerson, + Post as OCDPost) from opencivicdata.legislative.models import (Event as OCDEvent, Bill as OCDBill) from councilmatic_core.models import (Organization as CouncilmaticOrganization, Person as CouncilmaticPerson, Event as CouncilmaticEvent, - Bill as CouncilmaticBill) + Bill as CouncilmaticBill, + Post as CouncilmaticPost) @receiver(post_save, sender=OCDOrganization) @@ -61,3 +63,9 @@ def create_councilmatic_bill(sender, instance, created, **kwargs): cb = CouncilmaticBill.objects.get(id=instance.id) else: cb = instance.councilmatic_bill + +@receiver(post_save, sender=OCDPost) +def create_councilmatic_post(sender, instance, created, **kwargs): + if created: + 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 81cf0637..adcc21a0 100644 --- a/councilmatic_core/views.py +++ b/councilmatic_core/views.py @@ -195,7 +195,7 @@ def map(self): feature = { 'type': 'Feature', - 'geometry': json.loads(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': json.loads(person.latest_council_membership.post.shape), + 'geometry': json.loads(person.latest_council_membership.post.shape.json), 'properties': { 'district': person.latest_council_membership.post.label, }