From 43024d0305f3849eed645cda1ff0bad9613ea62c Mon Sep 17 00:00:00 2001 From: Jan Baykara Date: Wed, 5 Mar 2025 12:26:20 +0000 Subject: [PATCH 1/6] Fix GeoJSON graphql serialisation --- hub/graphql/types/geojson.py | 22 ++++++++++++++-------- nextjs/src/__generated__/graphql.ts | 2 ++ nextjs/src/__generated__/zodSchema.ts | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/hub/graphql/types/geojson.py b/hub/graphql/types/geojson.py index 3a03cd33b..a8219d22c 100644 --- a/hub/graphql/types/geojson.py +++ b/hub/graphql/types/geojson.py @@ -45,6 +45,10 @@ class PointGeometry: # lng, lat coordinates: List[float] + @classmethod + def from_geodjango(cls, point: Point) -> "PointGeometry": + return PointGeometry(coordinates=[point.x, point.y]) + @strawberry.type class PointFeature(Feature): @@ -57,7 +61,7 @@ def from_geodjango( ) -> "PointFeature": return PointFeature( id=str(id), - geometry=PointGeometry(coordinates=point), + geometry=PointGeometry.from_geodjango(point), properties=properties, ) @@ -70,6 +74,10 @@ class PolygonGeometry: type: GeoJSONTypes.Polygon = GeoJSONTypes.Polygon coordinates: List[List[List[float]]] + @classmethod + def from_geodjango(cls, polygon: Polygon) -> "PolygonGeometry": + return cls(coordinates=polygon.coords) + @strawberry.type class PolygonFeature(Feature): @@ -82,21 +90,19 @@ def from_geodjango( ) -> "PolygonFeature": return PolygonFeature( id=str(id), - geometry=PolygonGeometry(coordinates=polygon), + geometry=PolygonGeometry.from_geodjango(polygon), properties=properties, ) -# - - @strawberry.type class MultiPolygonGeometry: type: GeoJSONTypes.MultiPolygon = GeoJSONTypes.MultiPolygon coordinates: JSON - def __init__(self, coordinates: MultiPolygon): - self.coordinates = coordinates.json + @classmethod + def from_geodjango(cls, multipolygon: MultiPolygon) -> "MultiPolygonGeometry": + return cls(coordinates=multipolygon.coords) @strawberry.type @@ -110,6 +116,6 @@ def from_geodjango( ) -> "MultiPolygonFeature": return MultiPolygonFeature( id=str(id), - geometry=MultiPolygonGeometry(coordinates=multipolygon.json), + geometry=MultiPolygonGeometry.from_geodjango(multipolygon), properties=properties, ) diff --git a/nextjs/src/__generated__/graphql.ts b/nextjs/src/__generated__/graphql.ts index a137878fa..183555eda 100644 --- a/nextjs/src/__generated__/graphql.ts +++ b/nextjs/src/__generated__/graphql.ts @@ -1493,6 +1493,7 @@ export type GroupedData = { export type GroupedDataCount = { __typename?: 'GroupedDataCount'; + area?: Maybe; areaData?: Maybe; category?: Maybe; columns?: Maybe>; @@ -2931,6 +2932,7 @@ export type StatisticsConfig = { queryId?: InputMaybe; returnColumns?: InputMaybe>; sourceIds?: InputMaybe>; + summaryCalculations?: InputMaybe; }; export type StrFilterLookup = { diff --git a/nextjs/src/__generated__/zodSchema.ts b/nextjs/src/__generated__/zodSchema.ts index 193ae542e..f92126f1c 100644 --- a/nextjs/src/__generated__/zodSchema.ts +++ b/nextjs/src/__generated__/zodSchema.ts @@ -510,7 +510,8 @@ export function StatisticsConfigSchema(): z.ZodObject Date: Thu, 6 Mar 2025 11:29:56 +0000 Subject: [PATCH 2/6] Allow statistics API to return GeoJSON --- hub/graphql/types/geojson.py | 6 +- hub/graphql/types/model_types.py | 151 +++++++++++++++++++++++-------- hub/graphql/utils.py | 12 +++ hub/models.py | 23 ++++- 4 files changed, 152 insertions(+), 40 deletions(-) diff --git a/hub/graphql/types/geojson.py b/hub/graphql/types/geojson.py index a8219d22c..a892afa8e 100644 --- a/hub/graphql/types/geojson.py +++ b/hub/graphql/types/geojson.py @@ -53,7 +53,7 @@ def from_geodjango(cls, point: Point) -> "PointGeometry": @strawberry.type class PointFeature(Feature): geometry: PointGeometry - properties: JSON + properties: Optional[JSON] = strawberry.field(default_factory=dict) @classmethod def from_geodjango( @@ -82,7 +82,7 @@ def from_geodjango(cls, polygon: Polygon) -> "PolygonGeometry": @strawberry.type class PolygonFeature(Feature): geometry: PolygonGeometry - properties: JSON + properties: Optional[JSON] = strawberry.field(default_factory=dict) @classmethod def from_geodjango( @@ -108,7 +108,7 @@ def from_geodjango(cls, multipolygon: MultiPolygon) -> "MultiPolygonGeometry": @strawberry.type class MultiPolygonFeature(Feature): geometry: MultiPolygonGeometry - properties: JSON + properties: Optional[JSON] = strawberry.field(default_factory=dict) @classmethod def from_geodjango( diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index ba44f2afe..82f37fd99 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -1,3 +1,4 @@ +import json import logging import urllib.parse from datetime import datetime @@ -31,6 +32,7 @@ ) from hub.graphql.context import HubDataLoaderContext from hub.graphql.dataloaders import ( + FieldDataLoaderFactory, FieldReturningListDataLoaderFactory, ReverseFKWithFiltersDataLoaderFactory, filterable_dataloader_resolver, @@ -40,7 +42,12 @@ from hub.graphql.types.electoral_commission import ElectoralCommissionPostcodeLookup from hub.graphql.types.geojson import MultiPolygonFeature, PointFeature from hub.graphql.types.postcodes import PostcodesIOResult -from hub.graphql.utils import attr_field, dict_key_field, fn_field +from hub.graphql.utils import ( + attr_field, + dict_key_field, + django_model_instance_to_strawberry_type, + fn_field, +) from hub.management.commands.import_mps import party_shades from utils.geo_reference import ( AnalyticalAreaType, @@ -514,7 +521,7 @@ class ConstituencyElectionResult: @strawberry.type class ConstituencyElectionStats: - json: strawberry.Private[dict] + json: strawberry.Private[dict] = None date: str result: str @@ -564,6 +571,30 @@ def second_party_result(self, info: Info) -> PartyResult: ) +@strawberry.type +class AreaFeatureAreaProperties: + name: str = dict_key_field() + gss: str = dict_key_field() + area_type_name: str = dict_key_field() + area_type_code: str = dict_key_field() + + +@strawberry.type +class AreaFeatureProperties: + area: AreaFeatureAreaProperties = dict_key_field() + data: Optional[JSON] = dict_key_field() + + +@strawberry.type +class AreaPolygonFeature(MultiPolygonFeature): + properties: AreaFeatureProperties = strawberry.field(default_factory=dict) + + +@strawberry.type +class AreaPointFeature(PointFeature): + properties: AreaFeatureProperties = strawberry.field(default_factory=dict) + + @strawberry_django.type(models.Area, filters=AreaFilter) class Area: id: auto @@ -572,10 +603,16 @@ class Area: gss: auto name: auto area_type: "AreaType" = strawberry_django_dataloaders.fields.auto_dataloader_field() - geometry: auto + geometry: JSON = strawberry_django.field( + resolver=lambda root: ( + json.loads(root.geometry) + if isinstance(root.geometry, str) + else root.geometry + ) + ) overlaps: auto # So that we can pass in properties to the geojson Feature objects - extra_geojson_properties: strawberry.Private[object] + geojson_feature_properties: strawberry.Private = None people: List[Person] = filterable_dataloader_resolver( filter_type=Optional[PersonFilter], field_name="person", @@ -631,31 +668,34 @@ async def last_election(self, info: Info) -> Optional[ConstituencyElectionResult return cer @strawberry_django.field - def polygon( - self, info: Info, with_parent_data: bool = False - ) -> Optional[MultiPolygonFeature]: + def polygon(self, info: Info) -> Optional[AreaPolygonFeature]: props = { - "name": self.name, - "gss": self.gss, - "id": self.gss, - "area_type": self.area_type, + "area": { + "name": self.name, + "gss": self.gss, + "area_type_name": self.area_type.name, + "area_type_code": self.area_type.code, + }, + "data": self.geojson_feature_properties, } - if with_parent_data and hasattr(self, "extra_geojson_properties"): - props["extra_geojson_properties"] = self.extra_geojson_properties - return MultiPolygonFeature.from_geodjango( + return AreaPolygonFeature.from_geodjango( multipolygon=self.polygon, id=self.gss, properties=props ) @strawberry_django.field - def point( - self, info: Info, with_parent_data: bool = False - ) -> Optional[PointFeature]: - props = {"name": self.name, "gss": self.gss} - if with_parent_data and hasattr(self, "extra_geojson_properties"): - props["extra_geojson_properties"] = self.extra_geojson_properties - - return PointFeature.from_geodjango( + def point(self, info: Info) -> Optional[AreaPointFeature]: + props = { + "area": { + "name": self.name, + "gss": self.gss, + "area_type_name": self.area_type.name, + "area_type_code": self.area_type.code, + }, + "data": self.geojson_feature_properties, + } + + return AreaPointFeature.from_geodjango( point=self.point, id=self.gss, properties=props ) @@ -700,6 +740,23 @@ class GroupedDataCount: formatted_count: Optional[str] = None area_data: Optional[strawberry.Private[Area]] = None is_percentage: bool = False + area: Optional[Area] = None + + @strawberry_django.field + async def area(self, info: Info) -> Optional[Area]: + if self.area_data: + area = await self.area_data + elif self.gss: + area_loader = FieldDataLoaderFactory.get_loader_class( + models.Area, field="gss", select_related=["area_type"] + ) + area = await area_loader(context=info.context).load(self.gss) + if area: + graphql_area: Area = await django_model_instance_to_strawberry_type( + area, Area + ) + graphql_area.geojson_feature_properties = self.row + return graphql_area @strawberry_django.type(models.GenericData, filters=CommonDataFilter) @@ -748,7 +805,6 @@ class GenericData(CommonData): public_url: auto description: auto image: auto - area: Optional[Area] postcode: auto remote_url: str = fn_field() @@ -767,14 +823,42 @@ def areas(self, info: Info) -> Optional[Area]: return list(models.Area.objects.filter(polygon__contains=self.point)) @strawberry_django.field - def area_from_point(self, area_type: str, info: Info) -> Optional[Area]: - if self.point is None: - return None + async def area( + self, type: Optional[AnalyticalAreaType], info: Info + ) -> Optional[Area]: + if type is None: + if self.area_id is not None: + area_by_id_loader = FieldDataLoaderFactory.get_loader_class( + models.Area, field="id", select_related=["area_type"] + ) + area = await area_by_id_loader(context=info.context).load(self.area_id) + else: + area = self.area + else: + gss = self.postcode_data["codes"].get(type.value, None) + if gss is None: + return None + area_loader = FieldDataLoaderFactory.get_loader_class( + models.Area, field="gss", select_related=["area_type"] + ) + area = await area_loader(context=info.context).load(gss) + if area: + graphql_area = await django_model_instance_to_strawberry_type(area, Area) + graphql_area.geojson_feature_properties = self.to_dict() + return graphql_area - # TODO: data loader for this - return models.Area.objects.filter( - polygon__contains=self.point, area_type__code=area_type - ).first() + @strawberry_django.field + async def areas_overlapping(self, info: Info) -> List[Area]: + if self.postcode_data is None: + return [] + + area_loader = FieldDataLoaderFactory.get_loader_class( + models.Area, field="gss", select_related=["area_type"] + ) + areas = await area_loader(context=info.context).load_many( + self.postcode_data["codes"].values() + ) + return [a for a in areas if a is not None] @strawberry.type @@ -1603,12 +1687,7 @@ def statistics_for_choropleth( fields_requested_by_resolver = [f.name for f in info.selected_fields[0].selections] # Start with fields requested by resolver - choropleth_statistics_columns = ["label", "gss"] - return_columns = [ - field - for field in fields_requested_by_resolver - if field in choropleth_statistics_columns - ] + return_columns = [] if "count" in fields_requested_by_resolver: # (Count will default to the count of records automatically.) diff --git a/hub/graphql/utils.py b/hub/graphql/utils.py index 19facee9e..4c8100ea7 100644 --- a/hub/graphql/utils.py +++ b/hub/graphql/utils.py @@ -1,6 +1,12 @@ +from typing import cast + +from django.forms.models import model_to_dict + import strawberry import strawberry_django +from asgiref.sync import sync_to_async from strawberry.types.info import Info +from strawberry.types.object_type import StrawberryObjectDefinition from utils.py import transform_dict_values_recursive @@ -40,3 +46,9 @@ def graphql_type_to_dict(value, delete_null_keys=False): lambda x: x if (x is not strawberry.UNSET) else None, delete_null_keys=delete_null_keys, ) + + +async def django_model_instance_to_strawberry_type( + instance, graphql_type: strawberry.type +): + return cast(graphql_type, instance) diff --git a/hub/models.py b/hub/models.py index 34c7d05ff..3c794639e 100644 --- a/hub/models.py +++ b/hub/models.py @@ -861,6 +861,25 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) + def to_dict(self): + return { + "id": self.id, + "postcode": self.postcode, + "first_name": self.first_name, + "last_name": self.last_name, + "full_name": self.full_name, + "email": self.email, + "phone": self.phone, + "start_time": self.start_time, + "end_time": self.end_time, + "public_url": self.public_url, + "social_url": self.social_url, + "address": self.address, + "title": self.title, + "description": self.description, + "json": self.json, + } + class Area(models.Model): mapit_id = models.CharField(max_length=30) @@ -2749,7 +2768,9 @@ async def deferred_import_all( priority_enum = None try: match len(members): - case _ if len(members) < settings.SUPER_QUICK_IMPORT_ROW_COUNT_THRESHOLD: + case _ if len( + members + ) < settings.SUPER_QUICK_IMPORT_ROW_COUNT_THRESHOLD: priority_enum = ProcrastinateQueuePriority.SUPER_QUICK case _ if len( members From 942ac05c373e62c14192c5a2172fce72b936d7e1 Mon Sep 17 00:00:00 2001 From: Jan Baykara Date: Thu, 6 Mar 2025 11:34:36 +0000 Subject: [PATCH 3/6] lint --- hub/graphql/types/model_types.py | 8 +++++--- hub/graphql/utils.py | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index 82f37fd99..49e8c8f6b 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -740,7 +740,6 @@ class GroupedDataCount: formatted_count: Optional[str] = None area_data: Optional[strawberry.Private[Area]] = None is_percentage: bool = False - area: Optional[Area] = None @strawberry_django.field async def area(self, info: Info) -> Optional[Area]: @@ -893,7 +892,10 @@ def imported_data_count_by_area( postcode_io_key=analytical_area_type.value, layer_ids=layer_ids, ) - return [GroupedDataCount(**datum) for datum in data] + return [ + GroupedDataCount(**datum, area_data=datum.get("area", None)) + for datum in data + ] @strawberry_django.field def imported_data_count_of_areas( @@ -940,7 +942,7 @@ def imported_data_count_for_area( ) if len(res) == 0: return None - return GroupedDataCount(**res[0]) + return GroupedDataCount(**res[0], area_data=res[0].get("area", None)) @strawberry.type diff --git a/hub/graphql/utils.py b/hub/graphql/utils.py index 4c8100ea7..1b5e359bb 100644 --- a/hub/graphql/utils.py +++ b/hub/graphql/utils.py @@ -1,12 +1,8 @@ from typing import cast -from django.forms.models import model_to_dict - import strawberry import strawberry_django -from asgiref.sync import sync_to_async from strawberry.types.info import Info -from strawberry.types.object_type import StrawberryObjectDefinition from utils.py import transform_dict_values_recursive From 0ea94ff317dd9e0b72b108c64506c4ce663b3d43 Mon Sep 17 00:00:00 2001 From: Jan Baykara Date: Thu, 6 Mar 2025 11:57:15 +0000 Subject: [PATCH 4/6] Query all area types for a genericdata record --- hub/graphql/types/model_types.py | 36 +++++++------------ hub/graphql/utils.py | 4 +-- .../populate_external_data_source_types.py | 4 +-- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index 49e8c8f6b..11abef106 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -676,7 +676,7 @@ def polygon(self, info: Info) -> Optional[AreaPolygonFeature]: "area_type_name": self.area_type.name, "area_type_code": self.area_type.code, }, - "data": self.geojson_feature_properties, + "data": self.geojson_feature_properties if hasattr(self, "geojson_feature_properties") else {}, } return AreaPolygonFeature.from_geodjango( @@ -692,7 +692,7 @@ def point(self, info: Info) -> Optional[AreaPointFeature]: "area_type_name": self.area_type.name, "area_type_code": self.area_type.code, }, - "data": self.geojson_feature_properties, + "data": self.geojson_feature_properties if hasattr(self, "geojson_feature_properties") else {}, } return AreaPointFeature.from_geodjango( @@ -751,9 +751,7 @@ async def area(self, info: Info) -> Optional[Area]: ) area = await area_loader(context=info.context).load(self.gss) if area: - graphql_area: Area = await django_model_instance_to_strawberry_type( - area, Area - ) + graphql_area: Area = django_model_instance_to_strawberry_type(area, Area) graphql_area.geojson_feature_properties = self.row return graphql_area @@ -812,28 +810,18 @@ class GenericData(CommonData): def postcode_data(self) -> Optional[PostcodesIOResult]: return benedict(self.postcode_data) - @strawberry_django.field - def areas(self, info: Info) -> Optional[Area]: - if self.point is None: - return None - - # TODO: data loader for this - # Convert to list to make deeper async resolvers work - return list(models.Area.objects.filter(polygon__contains=self.point)) - @strawberry_django.field async def area( - self, type: Optional[AnalyticalAreaType], info: Info + self, info: Info, type: Optional[AnalyticalAreaType] = None ) -> Optional[Area]: + area = None if type is None: if self.area_id is not None: area_by_id_loader = FieldDataLoaderFactory.get_loader_class( models.Area, field="id", select_related=["area_type"] ) area = await area_by_id_loader(context=info.context).load(self.area_id) - else: - area = self.area - else: + elif self.postcode_data is not None: gss = self.postcode_data["codes"].get(type.value, None) if gss is None: return None @@ -842,12 +830,12 @@ async def area( ) area = await area_loader(context=info.context).load(gss) if area: - graphql_area = await django_model_instance_to_strawberry_type(area, Area) + graphql_area = django_model_instance_to_strawberry_type(area, Area) graphql_area.geojson_feature_properties = self.to_dict() return graphql_area @strawberry_django.field - async def areas_overlapping(self, info: Info) -> List[Area]: + async def areas(self, info: Info) -> List[Area]: if self.postcode_data is None: return [] @@ -1624,9 +1612,9 @@ def generic_data_by_external_data_source( "can_display_details" ): raise ValueError(f"User {user} does not have permission to view points") - return models.GenericData.objects.filter( - data_type__data_set__external_data_source=external_data_source - ) + qs = external_data_source.get_import_data() + + return list(qs) def generic_data_from_source_about_area( @@ -1650,7 +1638,7 @@ def generic_data_from_source_about_area( stats.filter_generic_data_using_gss_code(gss, mode) ) - return qs + return list(qs) def statistics( diff --git a/hub/graphql/utils.py b/hub/graphql/utils.py index 1b5e359bb..8b2748dc8 100644 --- a/hub/graphql/utils.py +++ b/hub/graphql/utils.py @@ -44,7 +44,5 @@ def graphql_type_to_dict(value, delete_null_keys=False): ) -async def django_model_instance_to_strawberry_type( - instance, graphql_type: strawberry.type -): +def django_model_instance_to_strawberry_type(instance, graphql_type: strawberry.type): return cast(graphql_type, instance) diff --git a/hub/management/commands/populate_external_data_source_types.py b/hub/management/commands/populate_external_data_source_types.py index ed15dcbf4..23f02ef85 100644 --- a/hub/management/commands/populate_external_data_source_types.py +++ b/hub/management/commands/populate_external_data_source_types.py @@ -39,9 +39,7 @@ def handle(self, id, *args, **options): f"Processing source {i + 1} of {source_count}: {source} ({source.id})" ) source: ExternalDataSource - qs: list[GenericData] = GenericData.objects.filter( - data_type__data_set__external_data_source_id=source.id - ).order_by("id") + qs: list[GenericData] = source.get_import_data().order_by("id") source_column_types = {} data_count = qs.count() From dcd1f398af8b9dab9983c43de041be5633378961 Mon Sep 17 00:00:00 2001 From: Jan Baykara Date: Thu, 6 Mar 2025 12:02:20 +0000 Subject: [PATCH 5/6] Fix error --- hub/graphql/types/model_types.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index 11abef106..b92c94a8d 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -676,7 +676,11 @@ def polygon(self, info: Info) -> Optional[AreaPolygonFeature]: "area_type_name": self.area_type.name, "area_type_code": self.area_type.code, }, - "data": self.geojson_feature_properties if hasattr(self, "geojson_feature_properties") else {}, + "data": ( + self.geojson_feature_properties + if hasattr(self, "geojson_feature_properties") + else {} + ), } return AreaPolygonFeature.from_geodjango( @@ -692,7 +696,11 @@ def point(self, info: Info) -> Optional[AreaPointFeature]: "area_type_name": self.area_type.name, "area_type_code": self.area_type.code, }, - "data": self.geojson_feature_properties if hasattr(self, "geojson_feature_properties") else {}, + "data": ( + self.geojson_feature_properties + if hasattr(self, "geojson_feature_properties") + else {} + ), } return AreaPointFeature.from_geodjango( From e851bbd076a07af669ad7548b52d616c3ff4dbf5 Mon Sep 17 00:00:00 2001 From: Jan Baykara Date: Thu, 6 Mar 2025 12:36:07 +0000 Subject: [PATCH 6/6] Allow typed generic data properties in GenericData GeoJSON --- hub/graphql/types/model_types.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index b92c94a8d..1acac45fa 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -583,6 +583,7 @@ class AreaFeatureAreaProperties: class AreaFeatureProperties: area: AreaFeatureAreaProperties = dict_key_field() data: Optional[JSON] = dict_key_field() + generic_data: Optional["GenericData"] = dict_key_field() @strawberry.type @@ -613,6 +614,7 @@ class Area: overlaps: auto # So that we can pass in properties to the geojson Feature objects geojson_feature_properties: strawberry.Private = None + geojson_feature_genericdata: strawberry.Private["GenericData"] = None people: List[Person] = filterable_dataloader_resolver( filter_type=Optional[PersonFilter], field_name="person", @@ -681,6 +683,11 @@ def polygon(self, info: Info) -> Optional[AreaPolygonFeature]: if hasattr(self, "geojson_feature_properties") else {} ), + "generic_data": ( + self.geojson_feature_genericdata + if hasattr(self, "geojson_feature_genericdata") + else None + ), } return AreaPolygonFeature.from_geodjango( @@ -701,6 +708,11 @@ def point(self, info: Info) -> Optional[AreaPointFeature]: if hasattr(self, "geojson_feature_properties") else {} ), + "generic_data": ( + self.geojson_feature_genericdata + if hasattr(self, "geojson_feature_genericdata") + else None + ), } return AreaPointFeature.from_geodjango( @@ -839,7 +851,7 @@ async def area( area = await area_loader(context=info.context).load(gss) if area: graphql_area = django_model_instance_to_strawberry_type(area, Area) - graphql_area.geojson_feature_properties = self.to_dict() + graphql_area.geojson_feature_genericdata = self return graphql_area @strawberry_django.field @@ -1676,6 +1688,10 @@ def statistics_for_choropleth( map_bounds: Optional[stats.MapBounds] = None, ) -> List[GroupedDataCount]: choropleth_config = choropleth_config or stats.ChoroplethConfig() + stats_config = stats_config or stats.StatisticsConfig() + + if not stats_config.group_by_area: + raise ValueError("An area type must be specified for a choropleth") user = get_current_user(info) for source in stats_config.source_ids: