Skip to content

Commit 6134ff6

Browse files
committed
Fix for missing IMD data in downloads
1 parent f09aee3 commit 6134ff6

File tree

7 files changed

+142
-97
lines changed

7 files changed

+142
-97
lines changed

findthatpostcode/blueprints/points.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ def get(latlon):
3636
filetype,
3737
"postcode.html.j2",
3838
point=result,
39+
stats=result.relationships["nearest_postcode"].get_stats(),
3940
)
4041
return return_result(result, filetype, "postcode.html.j2")

findthatpostcode/blueprints/postcodes.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from findthatpostcode.blueprints.utils import return_result
66
from findthatpostcode.controllers.postcodes import Postcode
77
from findthatpostcode.db import get_db
8-
from findthatpostcode.metadata import STATS_FIELDS
8+
from findthatpostcode.metadata import (
9+
OAC11_CODE,
10+
RU11IND_CODES,
11+
RUC21_CODES,
12+
STATS_FIELDS,
13+
)
914

1015
bp = Blueprint("postcodes", __name__, url_prefix="/postcodes")
1116

@@ -23,7 +28,7 @@ def postcode_redirect():
2328
def get_postcode(postcode, filetype="json"):
2429
es = get_db()
2530
result = Postcode.get_from_es(postcode, es)
26-
return return_result(result, filetype, "postcode.html.j2")
31+
return return_result(result, filetype, "postcode.html.j2", stats=result.get_stats())
2732

2833

2934
@bp.route("/hash/<hash>")
@@ -40,7 +45,7 @@ def multi_hash():
4045
return jsonify({"data": get_postcode_by_hash(hashes, fields)})
4146

4247

43-
def get_postcode_by_hash(hashes, fields):
48+
def get_postcode_by_hash(hashes: str | list[str], fields: list[str]):
4449
es = get_db()
4550

4651
if not isinstance(hashes, list):
@@ -59,17 +64,19 @@ def get_postcode_by_hash(hashes, fields):
5964
)
6065

6166
name_fields = [i.replace("_name", "") for i in fields if i.endswith("_name")]
62-
extra_fields = []
63-
stats = [i for i in STATS_FIELDS if i[0] in fields]
67+
stats_fields = []
68+
stats = [field for field in STATS_FIELDS if field.id in fields]
6469
if stats:
65-
extra_fields.append("lsoa11")
70+
for field in stats:
71+
if field.area not in stats_fields:
72+
stats_fields.append(field.area)
6673

6774
results = list(
6875
scan(
6976
es,
7077
index="geo_postcode",
7178
query={"query": {"bool": {"should": query}}},
72-
_source_includes=fields + name_fields + extra_fields,
79+
_source_includes=fields + name_fields + stats_fields,
7380
)
7481
)
7582
areas = scan(
@@ -81,39 +88,45 @@ def get_postcode_by_hash(hashes, fields):
8188
areanames = {i["_id"]: i["_source"].get("name") for i in areas}
8289

8390
def get_names(data):
84-
return {
85-
i: areanames.get(data.get(i.replace("_name", "")))
86-
for i in fields
87-
if i.endswith("_name")
88-
}
89-
90-
lsoas = {}
91-
92-
def get_stats(data):
93-
lsoa = data.get("lsoa11")
94-
if not lsoa or not stats or lsoa not in lsoas:
95-
return {}
96-
return {i[0]: dig_get(lsoas[lsoa], i[3]) for i in stats}
91+
names = {}
92+
for i in name_fields:
93+
names[f"{i}_name"] = None
94+
if i == "oac11":
95+
oac_name = OAC11_CODE.get(data.get(i))
96+
if oac_name:
97+
names[f"{i}_name"] = " > ".join(oac_name)
98+
elif i == "ru11ind":
99+
names[f"{i}_name"] = RU11IND_CODES.get(data.get(i))
100+
elif i == "ruc21":
101+
names[f"{i}_name"] = RUC21_CODES.get(data.get(i))
102+
else:
103+
names[f"{i}_name"] = areanames.get(data.get(i))
104+
return names
105+
106+
def get_stats(data, lsoas):
107+
result = {}
108+
for field in stats:
109+
lsoa_code = data.get(field.area)
110+
if not lsoa_code or not stats or lsoa_code not in lsoas:
111+
continue
112+
result[field.id] = dig_get(lsoas[lsoa_code], field.location)
113+
return result
97114

98115
if results:
116+
lsoas = {}
99117
if stats:
118+
lsoas_to_get = set()
119+
for r in results:
120+
for i in stats_fields:
121+
if r.get("_source", {}).get(i):
122+
lsoas_to_get.add(r.get("_source", {}).get(i))
100123
lsoas = {
101124
i["_id"]: i["_source"]
102125
for i in scan(
103126
es,
104127
index="geo_area",
105-
query={
106-
"query": {
107-
"terms": {
108-
"_id": [
109-
r.get("_source", {}).get("lsoa11")
110-
for r in results
111-
if r.get("_source", {}).get("lsoa11")
112-
]
113-
}
114-
}
115-
},
116-
_source_includes=[i[3] for i in stats],
128+
query={"query": {"terms": {"_id": list(lsoas_to_get)}}},
129+
_source_includes=[i.location for i in stats],
117130
)
118131
}
119132

@@ -122,7 +135,7 @@ def get_stats(data):
122135
"id": r["_id"],
123136
**r["_source"],
124137
**get_names(r["_source"]),
125-
**get_stats(r["_source"]),
138+
**get_stats(r["_source"], lsoas),
126139
}
127140
for r in results
128141
]

findthatpostcode/controllers/postcodes.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import re
22
from datetime import datetime
33

4+
from dictlib import dig_get
5+
46
from findthatpostcode.controllers.areas import Area
57
from findthatpostcode.controllers.controller import Controller
68
from findthatpostcode.controllers.places import Place
7-
from findthatpostcode.metadata import OAC11_CODE, RU11IND_CODES, RUC21_CODES
9+
from findthatpostcode.metadata import (
10+
OAC11_CODE,
11+
OTHER_CODES,
12+
RU11IND_CODES,
13+
RUC21_CODES,
14+
STATS_FIELDS,
15+
)
816

917

1018
class Postcode(Controller):
@@ -174,3 +182,19 @@ def toJSON(self, role="top"):
174182
json[0]["attributes"][i] = json[0]["attributes"][i].strftime("%Y-%m-%d")
175183

176184
return json
185+
186+
def get_stats(self):
187+
stats_block = {}
188+
for field in STATS_FIELDS:
189+
field_type = field.id.removesuffix("_rank").removesuffix("_decile")
190+
if self.attributes.get("ctry") is not None:
191+
stats_block[f"{field_type}_total"] = OTHER_CODES.get("imd", {}).get(
192+
f"{self.attributes['ctry']}-{field_type}"
193+
)
194+
if self.found and field.area in self.attributes:
195+
lsoa_code = self.attributes.get(field.area)
196+
for area in self.relationships.get("areas", []):
197+
if area.id == lsoa_code:
198+
stats_block[field.id] = dig_get(area.attributes, field.location)
199+
break
200+
return stats_block

findthatpostcode/metadata.py

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import os
33

4+
from findthatpostcode.utils import Field
5+
46
with open(
57
os.path.join(os.path.dirname(os.path.realpath(__file__)), "areatypes.json")
68
) as a:
@@ -67,55 +69,55 @@
6769

6870
DEFAULT_UPLOAD_FIELDS = ["latlng", "laua", "laua_name", "rgn", "rgn_name"]
6971
BASIC_UPLOAD_FIELDS = [
70-
("latlng", "Latitude / Longitude", False),
71-
("estnrth", "OS Easting / Northing", False),
72-
("pcds", "Standardised postcode", False),
73-
("oac11", "2011 Output Area Classification (OAC)", True),
74-
("ru11ind", "2011 Census rural-urban classification", True),
75-
("ruc21", "2021 Census rural-urban classification", True),
72+
Field(id="latlng", name="Latitude / Longitude", has_name=False),
73+
Field(id="estnrth", name="OS Easting / Northing", has_name=False),
74+
Field(id="pcds", name="Standardised postcode", has_name=False),
75+
Field(id="oac11", name="2011 Output Area Classification (OAC)", has_name=True),
76+
Field(id="ru11ind", name="2011 Census rural-urban classification", has_name=True),
77+
Field(id="ruc21", name="2021 Census rural-urban classification", has_name=True),
7678
]
7779
STATS_FIELDS = [
78-
(
79-
"imd2025_rank",
80-
"Index of multiple deprivation (2025) rank",
81-
False,
82-
"stats.imd2025.imd_rank",
80+
Field(
81+
id="imd2025_rank",
82+
name="Index of multiple deprivation (2025) rank",
83+
location="stats.imd2025.imd_rank",
84+
area="lsoa21",
8385
),
84-
(
85-
"imd2025_decile",
86-
"Index of multiple deprivation (2025) decile",
87-
False,
88-
"stats.imd2025.imd_decile",
86+
Field(
87+
id="imd2025_decile",
88+
name="Index of multiple deprivation (2025) decile",
89+
location="stats.imd2025.imd_decile",
90+
area="lsoa21",
8991
),
90-
(
91-
"imd2019_rank",
92-
"Index of multiple deprivation (2019) rank",
93-
False,
94-
"stats.imd2019.imd_rank",
92+
Field(
93+
id="imd2019_rank",
94+
name="Index of multiple deprivation (2019) rank",
95+
location="stats.imd2019.imd_rank",
96+
area="lsoa11",
9597
),
96-
(
97-
"imd2019_decile",
98-
"Index of multiple deprivation (2019) decile",
99-
False,
100-
"stats.imd2019.imd_decile",
98+
Field(
99+
id="imd2019_decile",
100+
name="Index of multiple deprivation (2019) decile",
101+
location="stats.imd2019.imd_decile",
102+
area="lsoa11",
101103
),
102-
(
103-
"imd2015_rank",
104-
"Index of multiple deprivation (2015) rank",
105-
False,
106-
"stats.imd2015.imd_rank",
104+
Field(
105+
id="imd2015_rank",
106+
name="Index of multiple deprivation (2015) rank",
107+
location="stats.imd2015.imd_rank",
108+
area="lsoa11",
107109
),
108-
(
109-
"imd2015_decile",
110-
"Index of multiple deprivation (2015) decile",
111-
False,
112-
"stats.imd2015.imd_decile",
110+
Field(
111+
id="imd2015_decile",
112+
name="Index of multiple deprivation (2015) decile",
113+
location="stats.imd2015.imd_decile",
114+
area="lsoa11",
113115
),
114-
# (
115-
# "popn",
116-
# "Total population (2015)",
117-
# False,
118-
# "stats.population2015.population_total"
116+
# Field(
117+
# id="popn2015",
118+
# name="Total population (2015)",
119+
# location="stats.population2015.population_total",
120+
# area="lsoa11",
119121
# ),
120122
]
121123

findthatpostcode/templates/addtocsv.html.j2

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@
9090
</tr>
9191
{% for b in basic_fields %}
9292
<tr>
93-
<td class="ph2">{{b[1]}}</td>
94-
<td class="ph2 tc"><input type="checkbox" autocomplete="off" name="fields" value="{{b[0]}}"
95-
{% if b[0] in default_fields %} checked="checked" {% endif %}></td>
93+
<td class="ph2">{{ b.name }}</td>
94+
<td class="ph2 tc"><input type="checkbox" autocomplete="off" name="fields" value="{{ b.id }}"
95+
{% if b.id in default_fields %} checked="checked" {% endif %}></td>
9696
<td class="ph2 tc">
97-
{% if b[2] %}
98-
<input type="checkbox" autocomplete="off" name="fields" value="{{b[0]}}_name"
99-
{% if '%s_name' % b[0] in default_fields %} checked="checked" {% endif %}>
97+
{% if b.has_name %}
98+
<input type="checkbox" autocomplete="off" name="fields" value="{{ b.id }}_name"
99+
{% if (b.id + '_name') in default_fields %} checked="checked" {% endif %}>
100100
{% endif %}
101101
</td>
102102
</tr>
@@ -108,9 +108,9 @@
108108
</tr>
109109
{% for b in stats_fields %}
110110
<tr>
111-
<td class="ph2">{{b[1]}}</td>
112-
<td class="ph2 tc"><input type="checkbox" autocomplete="off" name="fields" value="{{b[0]}}"
113-
{% if b[0] in default_fields %} checked="checked" {% endif %}></td>
111+
<td class="ph2">{{ b.name }}</td>
112+
<td class="ph2 tc"><input type="checkbox" autocomplete="off" name="fields" value="{{ b.id }}"
113+
{% if b.id in default_fields %} checked="checked" {% endif %}></td>
114114
<td class="ph2 tc">
115115
</td>
116116
</tr>

findthatpostcode/templates/postcode.html.j2

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,31 +111,26 @@
111111
{% endcall %}
112112
{% endif %}
113113

114-
{% for a in result.relationships.areas if a.attributes.stats and a.attributes.stats.imd2025 %}
115-
{% if loop.first %}
116-
{% set rank = a.attributes.stats.imd2025.imd_rank %}
117-
{% set total_25 = other_codes.imd[result.attributes.ctry + "-imd2025"] %}
118-
{% set total_19 = other_codes.imd[result.attributes.ctry + "-imd2019"] %}
119-
{% set total_15 = other_codes.imd[result.attributes.ctry + "-imd2015"] %}
114+
{% if stats.imd2025_rank is not none %}
120115
{% call info_block("Index of multiple deprivation (2025)") %}
121-
<p><strong>{{ "{:,.0f}".format(rank) }}</strong> out of {{ "{:,.0f}".format(total_25) }} lower super output areas in
116+
<p><strong>{{ "{:,.0f}".format(stats.imd2025_rank) }}</strong> out of {{ "{:,.0f}".format(stats.imd2025_total) }}
117+
lower super output areas in
122118
{{ result.attributes.ctry_name }} (where 1 is the most deprived LSOA).</p>
123-
<p><strong>{{ "{:.0%}".format( rank|float / total_25|float ) }}</strong> of LSOAs in
119+
<p><strong>{{ "{:.0%}".format( stats.imd2025_rank|float / stats.imd2025_total|float ) }}</strong> of LSOAs in
124120
{{ result.attributes.ctry_name }}
125121
are more deprived than this one.</p>
126-
{% if a.attributes.stats.imd2019 %}
122+
{% if stats.imd2019_rank %}
127123
<p>In <strong>2019</strong>
128-
{{ "{:.0%}".format( a.attributes.stats.imd2019.imd_rank|float / total_19|float ) }}</strong> of LSOAs in
124+
{{ "{:.0%}".format( stats.imd2019_rank|float / stats.imd2019_total|float ) }}</strong> of LSOAs in
129125
{{ result.attributes.ctry.name }} were more deprived than this one.</p>
130126
{% endif %}
131-
{% if a.attributes.stats.imd2015 %}
127+
{% if stats.imd2015_rank %}
132128
<p>In <strong>2015</strong>
133-
{{ "{:.0%}".format( a.attributes.stats.imd2015.imd_rank|float / total_15|float ) }}</strong> of LSOAs in
129+
{{ "{:.0%}".format( stats.imd2015_rank|float / stats.imd2015_total|float ) }}</strong> of LSOAs in
134130
{{ result.attributes.ctry.name }} were more deprived than this one.</p>
135131
{% endif %}
136132
{% endcall %}
137133
{% endif %}
138-
{% endfor %}
139134
</div>
140135

141136
{% for i in key_area_types[1:] %}

findthatpostcode/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class Field:
6+
id: str
7+
name: str
8+
location: str | None = None
9+
has_name: bool = False
10+
area: str | None = None

0 commit comments

Comments
 (0)