Skip to content

Commit

Permalink
Merge branch '487-display-local-councils' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
struan committed Mar 11, 2024
2 parents 4187b29 + eb5b627 commit d2902f0
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 60 deletions.
4 changes: 2 additions & 2 deletions hub/fixtures/areas.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"fields": {
"name": "Constituency",
"code": "WMC",
"area_type": "Constituency",
"area_type": "Westminster Constituency",
"description": "Constituency"
}
},
Expand All @@ -15,7 +15,7 @@
"fields": {
"name": "Constituency 2023",
"code": "WMC23",
"area_type": "Constituency 2023",
"area_type": "Westminster Constituency",
"description": "Constituency 2023"
}
},
Expand Down
93 changes: 59 additions & 34 deletions hub/management/commands/import_areas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,71 @@
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."
)

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()
8 changes: 7 additions & 1 deletion hub/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

"""
Expand Down
8 changes: 6 additions & 2 deletions hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 21 additions & 1 deletion hub/static/js/explore.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
}

Expand Down Expand Up @@ -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] ) {
Expand Down Expand Up @@ -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 => {
Expand Down
2 changes: 2 additions & 0 deletions hub/templates/hub/area.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,11 @@ <h3 class="h5"><a href="{% url 'area' area_type=overlap_constituency.old_area.ar
</a>
</li>
{% endif %}
{% if is_westminster_cons %}
<li class="nav-item">
<a class="nav-link" href="#mp">{% if area_type == "WMC23" %}PPCs{% else %}MP{% endif %}</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="#opinion">Public opinion</a>
</li>
Expand Down
10 changes: 5 additions & 5 deletions hub/templates/hub/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,16 @@ <h3 class="h6 mb-0 me-auto">${ column.title }</h3>
</template>
<template v-else-if="filters_applied && area_count == 0">
{% include 'hub/includes/icons/exclamation.html' with classes="flex-grow-0 me-2" %}
No matching constituencies
No matching ${ area_header_label }
</template>
<template v-else-if="filters_applied">
${ area_count } matching constituencies
${ area_count } matching ${ area_header_label }
</template>
<template v-else-if="area_count">
${ area_count } constituencies
${ area_count } ${ area_header_label }
</template>
<template v-else>
No constituencies
No ${ area_header_label }
</template>
</div>
<div class="explore-map" ref="map" :hidden="!mapViewActive"></div>
Expand All @@ -241,7 +241,7 @@ <h3 class="h6 mb-0 me-auto">${ column.title }</h3>
<tbody>
<tr v-for="(row, rowKey) in sortedTable" v-bind:key="'row-'+rowKey">
<td v-for="(column, columnKey) in table.meta.fields" v-bind:key="'row-'+rowKey+'-column-'+columnKey">
<a v-if="column == 'Constituency Name'" :href="`/area/WMC/${ sortedTable[rowKey][column] }`">
<a v-if="column == 'Constituency Name' || column == 'Council Name'" :href="`/area/${ area_type }/${ sortedTable[rowKey][column] }`">
${ sortedTable[rowKey][column] }
</a>
<span v-else>${ sortedTable[rowKey][column] }</span>
Expand Down
30 changes: 19 additions & 11 deletions hub/tests/test_import_areas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions hub/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion hub/views/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = [
{
Expand Down
20 changes: 19 additions & 1 deletion hub/views/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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 = {}
Expand Down

0 comments on commit d2902f0

Please sign in to comment.