From aa6552de1d1a190d8b760f36dc28102034f65eaf Mon Sep 17 00:00:00 2001 From: Michael Maurizi Date: Wed, 27 Jun 2018 16:34:41 -0400 Subject: [PATCH] Move dynamic checking of which datasets a City supports to a DB field The current dynamic check is not working very well. It checks for the presence of map cells and reports their dataset names, but doesn't check the values for those cells. In practice, this means that a location without LOCA data will be reported as supporting LOCA data, as the ingest will create LOCA cells, but fill them with NaN (not a number) values. Since checking the values in each cell would be computationally expensive, we've instead moved the list of supported datasets to be stored in the database. A future enhancement (#823) will ensure that this is properly set for every existing city. Closes #822 --- .../climate_change_api/settings.py | 1 + .../climate_change_api/climate_data/admin.py | 10 ++++++ .../migrations/0066_auto_20180627_2016.py | 31 +++++++++++++++++++ .../climate_change_api/climate_data/models.py | 29 +++++++++++++++-- .../climate_data/serializers.py | 5 --- .../climate_change_api/climate_data/views.py | 4 +-- 6 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 django/climate_change_api/climate_data/admin.py create mode 100644 django/climate_change_api/climate_data/migrations/0066_auto_20180627_2016.py diff --git a/django/climate_change_api/climate_change_api/settings.py b/django/climate_change_api/climate_change_api/settings.py index 113e4dda..b30a1a16 100644 --- a/django/climate_change_api/climate_change_api/settings.py +++ b/django/climate_change_api/climate_change_api/settings.py @@ -70,6 +70,7 @@ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.gis', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', diff --git a/django/climate_change_api/climate_data/admin.py b/django/climate_change_api/climate_data/admin.py new file mode 100644 index 00000000..df04cf3c --- /dev/null +++ b/django/climate_change_api/climate_data/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from climate_data.models import City + + +class CityAdmin(admin.ModelAdmin): + exclude = ('_geog',) + + +admin.site.register(City, CityAdmin) diff --git a/django/climate_change_api/climate_data/migrations/0066_auto_20180627_2016.py b/django/climate_change_api/climate_data/migrations/0066_auto_20180627_2016.py new file mode 100644 index 00000000..b7a5e650 --- /dev/null +++ b/django/climate_change_api/climate_data/migrations/0066_auto_20180627_2016.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-06-27 20:16 +from __future__ import unicode_literals + +import climate_data.models +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('climate_data', '0065_scenario_alias'), + ] + + operations = [ + migrations.AlterModelOptions( + name='city', + options={'verbose_name_plural': 'cities'}, + ), + migrations.AddField( + model_name='city', + name='datasets', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('LOCA', 'LOCA'), ('NEX-GDDP', 'NEX-GDDP')], max_length=48), default=climate_data.models.get_datasets, size=2), + ), + migrations.AlterField( + model_name='climatedataset', + name='name', + field=models.CharField(choices=[('LOCA', 'LOCA'), ('NEX-GDDP', 'NEX-GDDP')], max_length=48, unique=True), + ), + ] diff --git a/django/climate_change_api/climate_data/models.py b/django/climate_change_api/climate_data/models.py index 64ae71c8..43f68d6a 100644 --- a/django/climate_change_api/climate_data/models.py +++ b/django/climate_change_api/climate_data/models.py @@ -1,6 +1,6 @@ from django.contrib.gis.db import models from django.contrib.postgres.fields.array import ArrayField -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db.models import CASCADE, SET_NULL from climate_data.geo_boundary import census @@ -16,10 +16,23 @@ def db_type(self, connection): return models.SmallIntegerField().db_type(connection=connection) +def get_datasets(): + return [ClimateDataset.Datasets.LOCA, ClimateDataset.Datasets.NEX_GDDP] + + class ClimateDataset(models.Model): """Model representing a particular climate projection dataset.""" - name = models.CharField(max_length=48, unique=True) + class Datasets: + LOCA = 'LOCA' + NEX_GDDP = 'NEX-GDDP' + + CHOICES = ( + (LOCA, LOCA), + (NEX_GDDP, NEX_GDDP), + ) + + name = models.CharField(max_length=48, unique=True, choices=Datasets.CHOICES) label = models.CharField(max_length=128, blank=True, null=True) description = models.CharField(max_length=4096, blank=True, null=True) url = models.URLField(blank=True, null=True) @@ -286,6 +299,12 @@ class City(models.Model): is_coastal = models.BooleanField(default=False) population = models.IntegerField(null=True) + datasets = ArrayField( + models.CharField(max_length=48, choices=ClimateDataset.Datasets.CHOICES), + size=2, + default=get_datasets + ) + region = models.ForeignKey(Region, on_delete=SET_NULL, null=True) objects = CityManager() @@ -296,6 +315,7 @@ def __str__(self): class Meta: unique_together = ('name', 'admin') + verbose_name_plural = 'cities' def get_map_cell(self, dataset): """Get the map cell for a given dataset for a given city. @@ -309,6 +329,11 @@ def get_map_cell(self, dataset): def natural_key(self): return (self.name, self.admin) + def clean(self): + super().clean() + if len(self.datasets) != len(set(self.datasets)): + raise ValidationError({'datasets': 'Cannot contain duplicate datasets'}) + def save(self, *args, **kwargs): """Override save to keep the geography field up to date.""" self._geog = self.geom diff --git a/django/climate_change_api/climate_data/serializers.py b/django/climate_change_api/climate_data/serializers.py index 29c51761..37afe522 100644 --- a/django/climate_change_api/climate_data/serializers.py +++ b/django/climate_change_api/climate_data/serializers.py @@ -62,11 +62,6 @@ class Meta: class CitySerializer(GeoFeatureModelSerializer): - datasets = serializers.SerializerMethodField() - - def get_datasets(self, obj): - return [map_cell.dataset.name for map_cell in obj.map_cell_set.select_related('dataset')] - proximity = serializers.SerializerMethodField() def get_proximity(self, obj): diff --git a/django/climate_change_api/climate_data/views.py b/django/climate_change_api/climate_data/views.py index ae232a1b..f09c6d43 100644 --- a/django/climate_change_api/climate_data/views.py +++ b/django/climate_change_api/climate_data/views.py @@ -155,9 +155,7 @@ def datasets(self, request, pk=None): Returns 404 if the city object has no valid map cells. """ city = self.get_object() - map_cells = ClimateDataCityCell.objects.filter(city=city) - response = [map_cell.dataset.name for map_cell in map_cells] - return Response(response, status=status.HTTP_200_OK) + return Response(city.datasets, status=status.HTTP_200_OK) class CityMapCellListView(APIView):