From fe985395c29a52d8187e62b99cc5d4743c9e4afa Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Tue, 14 Nov 2023 11:30:18 +0000 Subject: [PATCH 1/3] add overlaps to area model --- .../0058_areaoverlap_area_overlaps.py | 51 +++++++++++++++++++ hub/models.py | 12 +++++ 2 files changed, 63 insertions(+) create mode 100644 hub/migrations/0058_areaoverlap_area_overlaps.py diff --git a/hub/migrations/0058_areaoverlap_area_overlaps.py b/hub/migrations/0058_areaoverlap_area_overlaps.py new file mode 100644 index 000000000..10bef0175 --- /dev/null +++ b/hub/migrations/0058_areaoverlap_area_overlaps.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.5 on 2023-11-14 11:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("hub", "0057_populate_dataset_areas_available"), + ] + + operations = [ + migrations.CreateModel( + name="AreaOverlap", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("population_overlap", models.SmallIntegerField(default=0)), + ("area_overlap", models.SmallIntegerField(default=0)), + ( + "area_new", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="new_overlaps", + to="hub.area", + ), + ), + ( + "area_old", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="old_overlaps", + to="hub.area", + ), + ), + ], + ), + migrations.AddField( + model_name="area", + name="overlaps", + field=models.ManyToManyField(through="hub.AreaOverlap", to="hub.area"), + ), + ] diff --git a/hub/models.py b/hub/models.py index f46d813cc..bdfb4e685 100644 --- a/hub/models.py +++ b/hub/models.py @@ -502,6 +502,7 @@ class Area(models.Model): name = models.CharField(max_length=200) area_type = models.ForeignKey(AreaType, on_delete=models.CASCADE) geometry = models.TextField(blank=True, null=True) + overlaps = models.ManyToManyField("self", through="AreaOverlap") def __str__(self): return self.name @@ -546,6 +547,17 @@ class Meta: unique_together = ["gss", "area_type"] +class AreaOverlap(models.Model): + area_old = models.ForeignKey( + Area, on_delete=models.CASCADE, related_name="old_overlaps" + ) + area_new = models.ForeignKey( + Area, on_delete=models.CASCADE, related_name="new_overlaps" + ) + population_overlap = models.SmallIntegerField(default=0) + area_overlap = models.SmallIntegerField(default=0) + + class AreaData(CommonData): area = models.ForeignKey(Area, on_delete=models.CASCADE) From 9996047a68f9bb5bcb438acfe4d4c5061f18a49e Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Tue, 14 Nov 2023 11:32:24 +0000 Subject: [PATCH 2/3] import constituency overlaps with new constituencies --- .../commands/import_new_constituencies.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/hub/management/commands/import_new_constituencies.py b/hub/management/commands/import_new_constituencies.py index 295ba1594..a9e6ec1c0 100644 --- a/hub/management/commands/import_new_constituencies.py +++ b/hub/management/commands/import_new_constituencies.py @@ -4,9 +4,11 @@ from django.core.management.base import BaseCommand from django.db.utils import IntegrityError +from mysoc_dataset import get_dataset_df from tqdm import tqdm -from hub.models import Area, AreaType +from hub.models import Area, AreaOverlap, AreaType +from utils.constituency_mapping import get_overlap_df class Command(BaseCommand): @@ -57,3 +59,30 @@ def handle(self, quiet: bool = False, *args, **options): con["properties"]["type"] = "WMC23" a.geometry = json.dumps(con) a.save() + + constituency_lookup = ( + get_dataset_df( + repo_name="2025-constituencies", + package_name="parliament_con_2025", + version_name="latest", + file_name="parl_constituencies_2025.csv", + ) + .set_index("short_code")["gss_code"] + .to_dict() + ) + + df = get_overlap_df("PARL10", "PARL25") + for area in Area.objects.filter(area_type__code="WMC"): + overlap_constituencies = df.query("PARL10 == @area.gss") + for _, row in overlap_constituencies.iterrows(): + new_area = Area.objects.get( + area_type__code="WMC23", gss=constituency_lookup[row["PARL25"]] + ) + AreaOverlap.objects.get_or_create( + area_old=area, + area_new=new_area, + defaults={ + "population_overlap": int(row["percentage_overlap_pop"] * 100), + "area_overlap": int(row["percentage_overlap_area"] * 100), + }, + ) From 34bff861fbc1bcd183ba5fcd2f39e96e963780a3 Mon Sep 17 00:00:00 2001 From: Struan Donald Date: Tue, 14 Nov 2023 11:32:56 +0000 Subject: [PATCH 3/3] display overlaps on old and new westminster constituencies --- hub/templates/hub/area.html | 49 +++++++++++++++++++++++++------------ hub/views/area.py | 31 +++++++++-------------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/hub/templates/hub/area.html b/hub/templates/hub/area.html index 5242c0146..6da0203c6 100644 --- a/hub/templates/hub/area.html +++ b/hub/templates/hub/area.html @@ -41,24 +41,43 @@

{{ area.name }}

- {% if area_type == "WMC" %} -
-
-

This is a 2010 constituency

+ {% if area_type == "WMC" and area.overlaps %} +
+
+ {% if overlap_constituencies|length > 1 %} +

At the next election, this constituency will be divided into {{ overlap_constituencies|length|apnumber }} new constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:

+ {% else %} +

At the next election, this constituency will be replaced with:

+ {% endif %} +
+ {% for overlap_constituency in overlap_constituencies %} +
+

{{ overlap_constituency.new_area }}

+

Will cover approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.

+
+ {% endfor %} +
+
+ {% elif area_type == "WMC23" and area.overlaps %} +
+ {% if area.overlaps %}
-

At the next election, people from this constituency will be divided into {{ overlap_constituencies|length|apnumber }} constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:

- - - {% for overlap_constituency in overlap_constituencies %} - - - - - {% endfor %} - -
{{ overlap_constituency.area }}Covers approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.
+ {% if overlap_constituencies|length > 1 %} +

This constituency will only exist at the next election, and replaces the previous {{ overlap_constituencies|length|apnumber }} constituenc{% if overlap_constituencies|length_is:1 %}y{% else %}ies{% endif %}:

+ {% else %} +

This constituency will only exist at the next election, and replaces:

+ {% endif %} +
+ {% for overlap_constituency in overlap_constituencies %} +
+

{{ overlap_constituency.old_area }}

+

Covered approximately {{ overlap_constituency.pop_overlap }}% of this constituency’s population, and {{ overlap_constituency.area_overlap }}% of this constituency’s area.

+
+ {% endfor %} +
+ {% endif %}
{% endif %} diff --git a/hub/views/area.py b/hub/views/area.py index 85072a832..a78759e91 100644 --- a/hub/views/area.py +++ b/hub/views/area.py @@ -5,8 +5,6 @@ from django.shortcuts import get_object_or_404, redirect from django.views.generic import DetailView, TemplateView, View -from mysoc_dataset import get_dataset_df - from hub.mixins import TitleMixin from hub.models import ( Area, @@ -18,7 +16,6 @@ UserDataSets, ) from utils import is_valid_postcode -from utils.constituency_mapping import get_overlap_df from utils.mapit import ( BadRequestException, ForbiddenException, @@ -180,27 +177,21 @@ def get_tags(self, mp_data, area_data): return tags def get_overlap_info(self, **kwargs): - # Get lookup between short code and GSS - constituency_lookup = ( - get_dataset_df( - repo_name="2025-constituencies", - package_name="parliament_con_2025", - version_name="latest", - file_name="parl_constituencies_2025.csv", - ) - .set_index("short_code")["gss_code"] - .to_dict() - ) + if self.object.area_type.code == "WMC": + overlaps = self.object.old_overlaps.all() + elif self.object.area_type.code == "WMC23": + overlaps = self.object.new_overlaps.all() + else: + return [] - df = get_overlap_df("PARL10", "PARL25") - overlap_constituencies = df.query("PARL10 == @self.object.gss") overlap_constituencies = [ { - "area": Area.objects.get(gss=constituency_lookup[row["PARL25"]]), - "pop_overlap": int(row["percentage_overlap_pop"] * 100), - "area_overlap": int(row["percentage_overlap_area"] * 100), + "new_area": overlap.area_new, + "old_area": overlap.area_old, + "pop_overlap": overlap.population_overlap, + "area_overlap": overlap.area_overlap, } - for index, row in overlap_constituencies.iterrows() + for overlap in overlaps ] return overlap_constituencies