diff --git a/hub/fixtures/areas.json b/hub/fixtures/areas.json index 261dd53d2..a71ccf1ee 100644 --- a/hub/fixtures/areas.json +++ b/hub/fixtures/areas.json @@ -5,7 +5,7 @@ "fields": { "name": "Constituency", "code": "WMC", - "area_type": "Constituency", + "area_type": "Westminster Constituency", "description": "Constituency" } }, @@ -15,7 +15,7 @@ "fields": { "name": "Constituency 2023", "code": "WMC23", - "area_type": "Constituency 2023", + "area_type": "Westminster Constituency", "description": "Constituency 2023" } }, diff --git a/hub/management/commands/import_areas.py b/hub/management/commands/import_areas.py index 982f55742..fe7949f24 100644 --- a/hub/management/commands/import_areas.py +++ b/hub/management/commands/import_areas.py @@ -11,6 +11,30 @@ class Command(BaseCommand): help = "Import basic area information from MaPit" + boundary_types = [ + { + "mapit_type": ["WMC"], + "name": "2010 Parliamentary Constituency", + "code": "WMC", + "area_type": "Westminster Constituency", + "description": "Westminster Parliamentary Constituency boundaries, as created in 2010", + }, + { + "mapit_type": ["LBO", "UTA", "COI", "LGD", "CTY", "MTD"], + "name": "Single Tier Councils", + "code": "STC", + "area_type": "Single Tier Council", + "description": "Single Tier Council", + }, + { + "mapit_type": ["DIS", "NMD"], + "name": "District Councils", + "code": "DIS", + "area_type": "District Council", + "description": "District Council", + }, + ] + def add_arguments(self, parser): parser.add_argument( "-q", "--quiet", action="store_true", help="Silence progress bars." @@ -18,39 +42,40 @@ def add_arguments(self, parser): def handle(self, quiet: bool = False, *args, **options): mapit_client = mapit.MapIt() - areas = mapit_client.areas_of_type(["WMC"]) - area_type, created = AreaType.objects.get_or_create( - name="2010 Parliamentary Constituency", - code="WMC", - area_type="Westminster Constituency", - description="Westminster Parliamentary Constituency boundaries, as created in 2010", - ) - - if not quiet: - print("Importing Areas") - for area in tqdm(areas, disable=quiet): - try: - geom = mapit_client.area_geometry(area["id"]) - geom = { - "type": "Feature", - "geometry": geom, - "properties": { - "PCON13CD": area["codes"]["gss"], - "name": area["name"], - "type": "WMC", - }, - } - geom = json.dumps(geom) - except mapit.NotFoundException: # pragma: no cover - print(f"could not find mapit area for {area['name']}") - geom = None - - a, created = Area.objects.get_or_create( - mapit_id=area["id"], - gss=area["codes"]["gss"], - name=area["name"], - area_type=area_type, + for b_type in self.boundary_types: + areas = mapit_client.areas_of_type(b_type["mapit_type"]) + area_type, created = AreaType.objects.get_or_create( + name=b_type["name"], + code=b_type["code"], + area_type=b_type["area_type"], + description=b_type["description"], ) - a.geometry = geom - a.save() + if not quiet: + print("Importing Areas") + for area in tqdm(areas, disable=quiet): + try: + geom = mapit_client.area_geometry(area["id"]) + geom = { + "type": "Feature", + "geometry": geom, + "properties": { + "PCON13CD": area["codes"]["gss"], + "name": area["name"], + "type": b_type["code"], + }, + } + geom = json.dumps(geom) + except mapit.NotFoundException: # pragma: no cover + print(f"could not find mapit area for {area['name']}") + geom = None + + a, created = Area.objects.get_or_create( + mapit_id=area["id"], + gss=area["codes"]["gss"], + name=area["name"], + area_type=area_type, + ) + + a.geometry = geom + a.save() diff --git a/hub/mixins.py b/hub/mixins.py index 39aa1264a..2757e0f22 100644 --- a/hub/mixins.py +++ b/hub/mixins.py @@ -159,6 +159,8 @@ def format_value(self, type, value): def data(self, as_dict=False, mp_name=False): headers = ["Constituency Name"] + if self.area_type().area_type != "Westminster Constituency": + headers = ["Council Name"] headers += map(lambda f: f["dataset"].label, self.filters()) headers += map( lambda f: f.get("header_label", f["label"]), self.columns(mp_name=mp_name) @@ -181,13 +183,17 @@ def data(self, as_dict=False, mp_name=False): shortcut if no filters/columns were requested: just return a single column of constituency names """ - if not cols: + if not cols or len(cols) == 0: areas = Area.objects area_type = self.area_type() if area_type is not None: areas = areas.filter(area_type=area_type) for area in areas.order_by("name"): data.append([area.name]) + if as_dict: + for area in Area.objects.filter(id__in=area_ids): + area_data[area.name]["area"] = area + return area_data return data """ diff --git a/hub/models.py b/hub/models.py index 2252795e3..8373e882d 100644 --- a/hub/models.py +++ b/hub/models.py @@ -412,9 +412,13 @@ def filter(self, query, **kwargs): class AreaType(models.Model): - VALID_AREA_TYPES = ["WMC", "WMC23"] + VALID_AREA_TYPES = ["WMC", "WMC23", "STC", "DIS"] - AREA_TYPES = [("westminster_constituency", "Westminster Constituency")] + AREA_TYPES = [ + ("westminster_constituency", "Westminster Constituency"), + ("single_tier_council", "Single Tier Council"), + ("district_council", "District Council"), + ] name = models.CharField(max_length=50, unique=True) code = models.CharField(max_length=10, unique=True) area_type = models.CharField(max_length=50, choices=AREA_TYPES) diff --git a/hub/static/js/explore.esm.js b/hub/static/js/explore.esm.js index bb55e3423..7b3e77ace 100644 --- a/hub/static/js/explore.esm.js +++ b/hub/static/js/explore.esm.js @@ -29,7 +29,14 @@ const app = createApp({ }, { slug: "WMC23", label: "Future constituencies" + }, { + slug: "STC", + label: "Single Tier councils" + }, { + slug: "DIS", + label: "District councils" }], + area_header_label: "constituencies", filters_applied: false, // were filters applied on the last Update? area_count: 0, // number of areas returned on last Update @@ -305,7 +312,7 @@ const app = createApp({ geomUrl() { let url = new URL(window.location.origin + '/exploregeometry.json') - if (["WMC", "WMC23"].includes(this.area_type)) { + if (["WMC", "WMC23", "DIS", "STC"].includes(this.area_type)) { url = new URL(window.location.origin + '/exploregeometry/' + this.area_type + '.json') } @@ -471,6 +478,11 @@ const app = createApp({ }); this.area_count = Object.keys(features).length + if (["DIS", "STC"].includes(this.area_type)) { + this.area_header_label = "councils" + } else { + this.area_header_label = "constituencies" + } window.geojson.eachLayer(function (layer) { if ( features[layer.feature.properties.PCON13CD] ) { @@ -518,6 +530,14 @@ const app = createApp({ this.loading = true this.filters_applied = (this.filters.length > 0) + if (this.sortBy == 'Constituency Name' || this.sortBy == 'Council Name') { + if (["DIS", "STC"].includes(this.area_type)) { + this.sortBy = "Council Name" + } else { + this.sortBy = "Constituency Name" + } + } + fetch(this.url('/explore.csv')) .then(response => response.blob()) .then(data => { diff --git a/hub/templates/hub/area.html b/hub/templates/hub/area.html index 294c8afab..c5a3e6832 100644 --- a/hub/templates/hub/area.html +++ b/hub/templates/hub/area.html @@ -120,9 +120,11 @@

{% if area_type == "WMC23" %}PPCs{% else %}MP{% endif %} + {% endif %} diff --git a/hub/templates/hub/explore.html b/hub/templates/hub/explore.html index 31082fbca..c7253feeb 100644 --- a/hub/templates/hub/explore.html +++ b/hub/templates/hub/explore.html @@ -213,16 +213,16 @@

${ column.title }

@@ -241,7 +241,7 @@

${ column.title }

- + ${ sortedTable[rowKey][column] } ${ sortedTable[rowKey][column] } diff --git a/hub/tests/test_import_areas.py b/hub/tests/test_import_areas.py index 303ac7896..424b12583 100644 --- a/hub/tests/test_import_areas.py +++ b/hub/tests/test_import_areas.py @@ -7,17 +7,9 @@ from utils.mapit import MapIt -class ImportAreasTestCase(TestCase): - quiet_parameter: bool = False - - @mock.patch.object(MapIt, "areas_of_type") - @mock.patch.object(MapIt, "area_geometry") - def test_import(self, mapit_geom, mapit_areas): - mapit_geom.return_value = { - "type": "Polygon", - "coordinates": [[1, 2], [2, 1]], - } - mapit_areas.return_value = [ +def mock_areas_of_type(types): + if "WMC" in types: + return [ { "id": 1, "codes": {"gss": "E10000001", "unit_id": "1"}, @@ -33,6 +25,22 @@ def test_import(self, mapit_geom, mapit_areas): "type": "WMC", }, ] + + return [] + + +class ImportAreasTestCase(TestCase): + quiet_parameter: bool = False + + @mock.patch.object(MapIt, "areas_of_type") + @mock.patch.object(MapIt, "area_geometry") + def test_import(self, mapit_geom, mapit_areas): + mapit_geom.return_value = { + "type": "Polygon", + "coordinates": [[1, 2], [2, 1]], + } + mapit_areas.side_effect = mock_areas_of_type + call_command("import_areas", quiet=self.quiet_parameter) areas = Area.objects.all() diff --git a/hub/tests/test_views.py b/hub/tests/test_views.py index 96d93c9a0..fedfa4018 100644 --- a/hub/tests/test_views.py +++ b/hub/tests/test_views.py @@ -51,13 +51,13 @@ def test_explore_datasets_json_page(self): self.assertEqual(response.status_code, 200) datasets = response.json() - self.assertEqual(11, len(datasets)) + self.assertEqual(12, len(datasets)) self.client.logout() response = self.client.get(url) self.assertEqual(response.status_code, 200) datasets = response.json() - self.assertEqual(4, len(datasets)) + self.assertEqual(5, len(datasets)) def test_explore_view_with_many_to_one(self): url = f"{reverse('explore_csv')}?mp_appg_membership__exact=MadeUpAPPG" diff --git a/hub/views/area.py b/hub/views/area.py index 0167111d3..4654e5349 100644 --- a/hub/views/area.py +++ b/hub/views/area.py @@ -192,7 +192,12 @@ def get_context_data(self, **kwargs): ): if context["overlap_constituencies"][0].get("unchanged", False): context["overlap_unchanged"] = True - context["area_type"] = str(self.object.area_type) + area_type = self.object.area_type + context["area_type"] = area_type.code + context["is_westminster_cons"] = True + if area_type.area_type != "Westminster Constituency": + context["is_westminster_cons"] = False + if context["area_type"] == "WMC23": context["PPCs"] = [ { diff --git a/hub/views/explore.py b/hub/views/explore.py index 8d8ff2fb9..a0273df6b 100644 --- a/hub/views/explore.py +++ b/hub/views/explore.py @@ -155,6 +155,21 @@ def render_to_response(self, context, **response_kwargs): } ) + datasets.append( + { + "scope": "public", + "is_featured": False, + "is_favourite": False, + "is_filterable": False, + "is_shadable": False, + "category": "place", + "name": "gss", + "title": "Council GSS code", + "source_label": "Data from mySociety.", + "areas_available": ["DIS", "STC"], + } + ) + return JsonResponse(list(datasets), safe=False) @@ -205,7 +220,10 @@ class ExploreGeometryCachedJSON(ExploreGeometryJSON): class ExploreJSON(FilterMixin, TemplateView): def render_to_response(self, context, **response_kwargs): geom = [] - areas = self.data(as_dict=True, mp_name=True) + mp_name = True + if self.area_type().area_type != "Westminster Constituency": + mp_name = False + areas = self.data(as_dict=True, mp_name=mp_name) shader_areas = [a["area"] for a in areas.values()] shader = self.shader() colours = {}