Skip to content
Draft
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
176 changes: 176 additions & 0 deletions every_election/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
ModerationStatuses,
)
from organisations.models import (
DivisionGeography,
Organisation,
OrganisationDivision,
OrganisationDivisionSet,
)
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework_gis.serializers import (
GeoFeatureModelSerializer,
GeometrySerializerMethodField,
Expand Down Expand Up @@ -64,6 +66,51 @@ class Meta:
fields = org_fields


class OrganisationCurrentDivisionSetSerializer(serializers.ModelSerializer):
division_count = serializers.SerializerMethodField()

def get_division_count(self, obj):
return obj.divisions.count()

class Meta:
model = OrganisationDivisionSet
fields = (
"start_date",
"end_date",
"short_title",
"legislation_url",
"consultation_url",
"notes",
"division_count",
)


class OrganisationDetailSerializer(OrganisationSerializer):
"""Extends OrganisationSerializer with divisionset fields. Use prefetch_related("divisionset")
on the queryset to avoid N+1 queries when listing multiple organisations."""

current_divisionset = serializers.SerializerMethodField()
divisionsets_url = serializers.SerializerMethodField()

def get_current_divisionset(self, obj):
# Iterates the prefetch cache when available instead of issuing a new query.
active = [ds for ds in obj.divisionset.all() if ds.end_date is None]
if len(active) != 1:
return None
return OrganisationCurrentDivisionSetSerializer(active[0]).data

def get_divisionsets_url(self, obj):
request = self.context.get("request")
url = reverse("api:division-list")
full_url = f"{url}?org_slug={obj.slug}"
if request:
return request.build_absolute_uri(full_url)
return full_url

class Meta(OrganisationSerializer.Meta):
fields = org_fields + ("current_divisionset", "divisionsets_url")


class OrganisationGeoSerializer(GeoFeatureModelSerializer):
geography_model = GeometrySerializerMethodField()
url = OrganisationHyperlinkedIdentityField(
Expand Down Expand Up @@ -93,6 +140,8 @@ class Meta:


class OrganisationDivisionSerializer(serializers.ModelSerializer):
"""Base division serializer — used when a division is nested inside an election."""

divisionset = OrganisationDivisionSetSerializer()

class Meta:
Expand All @@ -112,6 +161,133 @@ class Meta:
)


class DivisionOrganisationSerializer(serializers.ModelSerializer):
class Meta:
model = Organisation
fields = (
"official_identifier",
"organisation_type",
"slug",
)


class FlatDivisionSerializer(OrganisationDivisionSerializer):
"""Division serializer for the flat /api/divisions/ endpoint — includes parent organisation."""

organisation = DivisionOrganisationSerializer(source="divisionset.organisation")

class Meta(OrganisationDivisionSerializer.Meta):
fields = (
"divisionset",
"organisation",
"name",
"official_identifier",
"slug",
"division_type",
"division_subtype",
"division_election_sub_type",
"seats_total",
"territory_code",
"created",
"modified",
)


class DivisionGeoSerializer(GeoFeatureModelSerializer):
geography_model = GeometrySerializerMethodField()

def get_geography_model(self, obj):
try:
return obj.geography.geography
except DivisionGeography.DoesNotExist:
return None

class Meta:
model = OrganisationDivision
geo_field = "geography_model"
fields = (
"name",
"official_identifier",
"slug",
"division_type",
"division_subtype",
"seats_total",
"territory_code",
)


class DivisionForDivisionSetSerializer(serializers.ModelSerializer):
geo_url = serializers.SerializerMethodField()
geography_url = serializers.SerializerMethodField()

def get_geo_url(self, obj):
request = self.context.get("request")
url = reverse("api:division-geo", args=[obj.pk])
if request:
return request.build_absolute_uri(url)
return url

def get_geography_url(self, obj):
return obj.format_geography_link()

class Meta:
model = OrganisationDivision
fields = (
"name",
"official_identifier",
"slug",
"division_type",
"division_subtype",
"division_election_sub_type",
"seats_total",
"territory_code",
"geo_url",
"geography_url",
)


class DivisionSetOrganisationSerializer(serializers.ModelSerializer):
url = OrganisationHyperlinkedIdentityField(
view_name="api:organisation-detail", read_only=True
)

class Meta:
model = Organisation
fields = (
"url",
"official_identifier",
"organisation_type",
"slug",
)


class DivisionSetSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="api:divisionset-detail", read_only=True
)
organisation = DivisionSetOrganisationSerializer()
divisions = serializers.SerializerMethodField()

def get_divisions(self, obj):
return DivisionForDivisionSetSerializer(
obj.divisions.all(), many=True, context=self.context
).data

class Meta:
model = OrganisationDivisionSet
fields = (
"url",
"organisation",
"start_date",
"end_date",
"short_title",
"legislation_url",
"consultation_url",
"notes",
"divisions",
)


class ElectionTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ElectionType
Expand Down
113 changes: 113 additions & 0 deletions every_election/apps/api/tests/test_api_divisions_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from datetime import date

from django.test import override_settings
from organisations.tests.factories import (
OrganisationDivisionFactory,
OrganisationDivisionSetFactory,
OrganisationFactory,
)
from rest_framework.test import APITestCase


class TestDivisionsEndpoint(APITestCase):
def setUp(self):
super().setUp()
self.local_org = OrganisationFactory(
official_identifier="STR",
organisation_type="local-authority",
start_date=date(2019, 4, 1),
end_date=None,
)
self.active_ds = OrganisationDivisionSetFactory(
organisation=self.local_org,
start_date=date(2019, 5, 2),
end_date=None,
short_title="Stroud 2019 Review",
)
self.historical_ds = OrganisationDivisionSetFactory(
organisation=self.local_org,
start_date=date(2010, 1, 1),
end_date=date(2019, 5, 1),
short_title="Stroud 2010 Review",
)
self.division_active = OrganisationDivisionFactory(
divisionset=self.active_ds,
name="Amberley",
)
self.division_historical = OrganisationDivisionFactory(
divisionset=self.historical_ds,
name="Old Ward",
)

self.other_org = OrganisationFactory(
official_identifier="PCC1",
organisation_type="combined-authority",
start_date=date(2017, 1, 1),
end_date=None,
)
self.other_ds = OrganisationDivisionSetFactory(
organisation=self.other_org,
start_date=date(2017, 1, 1),
end_date=None,
)
self.other_division = OrganisationDivisionFactory(
divisionset=self.other_ds,
name="Other Division",
)

def test_list_returns_200_with_pagination(self):
resp = self.client.get("/api/divisions/")
self.assertEqual(200, resp.status_code)
data = resp.json()
self.assertIn("count", data)
self.assertIn("results", data)

def test_list_result_shape(self):
resp = self.client.get("/api/divisions/")
data = resp.json()
result = data["results"][0]
self.assertIn("divisionset", result)
self.assertIn("organisation", result)
self.assertIn("name", result)

def test_filter_by_org_type(self):
resp = self.client.get("/api/divisions/?org_type=local-authority")
data = resp.json()
org_types = {
r["organisation"]["organisation_type"] for r in data["results"]
}
self.assertEqual({"local-authority"}, org_types)

def test_filter_by_org_slug(self):
resp = self.client.get(
f"/api/divisions/?org_slug={self.local_org.slug}"
)
data = resp.json()
slugs = {r["organisation"]["slug"] for r in data["results"]}
self.assertEqual({self.local_org.slug}, slugs)
# should include both active and historical divisionset divisions
self.assertEqual(2, data["count"])

def test_filter_current_excludes_historical(self):
resp = self.client.get("/api/divisions/?current=true")
data = resp.json()
for result in data["results"]:
self.assertIsNone(result["divisionset"]["end_date"])

def test_detail_endpoint(self):
resp = self.client.get(f"/api/divisions/{self.division_active.pk}/")
self.assertEqual(200, resp.status_code)
data = resp.json()
self.assertEqual("Amberley", data["name"])
self.assertIn("divisionset", data)
self.assertIn("organisation", data)
self.assertEqual("STR", data["organisation"]["official_identifier"])

@override_settings(DEBUG=False)
def test_query_count_select_related(self):
for i in range(10):
OrganisationDivisionFactory(divisionset=self.active_ds)

# With select_related, one query for count + one for data (no per-row extras)
with self.assertNumQueries(2):
self.client.get("/api/divisions/?limit=10")
Loading