Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions hub/graphql/types/geojson.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,23 @@ 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):
geometry: PointGeometry
properties: JSON
properties: Optional[JSON] = strawberry.field(default_factory=dict)

@classmethod
def from_geodjango(
cls, point: Point, properties: dict = {}, id: str = None
) -> "PointFeature":
return PointFeature(
id=str(id),
geometry=PointGeometry(coordinates=point),
geometry=PointGeometry.from_geodjango(point),
properties=properties,
)

Expand All @@ -70,46 +74,48 @@ 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):
geometry: PolygonGeometry
properties: JSON
properties: Optional[JSON] = strawberry.field(default_factory=dict)

@classmethod
def from_geodjango(
cls, polygon: Polygon, properties: dict = {}, id: str = None
) -> "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
class MultiPolygonFeature(Feature):
geometry: MultiPolygonGeometry
properties: JSON
properties: Optional[JSON] = strawberry.field(default_factory=dict)

@classmethod
def from_geodjango(
cls, multipolygon: MultiPolygon, properties: dict = {}, id: str = None
) -> "MultiPolygonFeature":
return MultiPolygonFeature(
id=str(id),
geometry=MultiPolygonGeometry(coordinates=multipolygon.json),
geometry=MultiPolygonGeometry.from_geodjango(multipolygon),
properties=properties,
)
191 changes: 142 additions & 49 deletions hub/graphql/types/model_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import urllib.parse
from datetime import datetime
Expand Down Expand Up @@ -31,6 +32,7 @@
)
from hub.graphql.context import HubDataLoaderContext
from hub.graphql.dataloaders import (
FieldDataLoaderFactory,
FieldReturningListDataLoaderFactory,
ReverseFKWithFiltersDataLoaderFactory,
filterable_dataloader_resolver,
Expand All @@ -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,
Expand Down Expand Up @@ -514,7 +521,7 @@ class ConstituencyElectionResult:

@strawberry.type
class ConstituencyElectionStats:
json: strawberry.Private[dict]
json: strawberry.Private[dict] = None

date: str
result: str
Expand Down Expand Up @@ -564,6 +571,31 @@ 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()
generic_data: Optional["GenericData"] = 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
Expand All @@ -572,10 +604,17 @@ 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
geojson_feature_genericdata: strawberry.Private["GenericData"] = None
people: List[Person] = filterable_dataloader_resolver(
filter_type=Optional[PersonFilter],
field_name="person",
Expand Down Expand Up @@ -631,31 +670,52 @@ 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 hasattr(self, "geojson_feature_properties")
else {}
),
"generic_data": (
self.geojson_feature_genericdata
if hasattr(self, "geojson_feature_genericdata")
else None
),
}
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
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(
point=self.point, id=self.gss, properties=props
)

Expand Down Expand Up @@ -701,6 +761,20 @@ class GroupedDataCount:
area_data: Optional[strawberry.Private[Area]] = None
is_percentage: bool = False

@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 = 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)
class GroupedData:
Expand Down Expand Up @@ -748,7 +822,6 @@ class GenericData(CommonData):
public_url: auto
description: auto
image: auto
area: Optional[Area]

postcode: auto
remote_url: str = fn_field()
Expand All @@ -758,23 +831,41 @@ 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))
async def area(
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)
elif self.postcode_data is not None:
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 = django_model_instance_to_strawberry_type(area, Area)
graphql_area.geojson_feature_genericdata = self
return graphql_area

@strawberry_django.field
def area_from_point(self, area_type: str, info: Info) -> Optional[Area]:
if self.point is None:
return None
async def areas(self, info: Info) -> List[Area]:
if self.postcode_data is None:
return []

# TODO: data loader for this
return models.Area.objects.filter(
polygon__contains=self.point, area_type__code=area_type
).first()
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
Expand Down Expand Up @@ -809,7 +900,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(
Expand Down Expand Up @@ -856,7 +950,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
Expand Down Expand Up @@ -1538,9 +1632,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(
Expand All @@ -1564,7 +1658,7 @@ def generic_data_from_source_about_area(
stats.filter_generic_data_using_gss_code(gss, mode)
)

return qs
return list(qs)


def statistics(
Expand Down Expand Up @@ -1594,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:
Expand All @@ -1603,12 +1701,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.)
Expand Down
Loading
Loading