diff --git a/findthatpostcode/commands/__init__.py b/findthatpostcode/commands/__init__.py index 5253392..355e97f 100644 --- a/findthatpostcode/commands/__init__.py +++ b/findthatpostcode/commands/__init__.py @@ -2,7 +2,7 @@ from findthatpostcode.db import init_db -from . import boundaries, codes, placenames, postcodes, stats +from . import boundaries, codes, placenames, postcodes, stats, utils @click.command("init-db") @@ -32,4 +32,13 @@ def import_group(): import_group.add_command(stats.import_imd2015) import_group.add_command(stats.import_imd2019) + +@cli.group(name="utils") +def utils_group(): + pass + + +utils_group.add_command(utils.sample_zip) + + cli.add_command(init_db_command) diff --git a/findthatpostcode/commands/postcodes.py b/findthatpostcode/commands/postcodes.py index 2e6fe63..c5014c7 100644 --- a/findthatpostcode/commands/postcodes.py +++ b/findthatpostcode/commands/postcodes.py @@ -1,6 +1,7 @@ """ Import commands for the register of geographic codes and code history database """ + import csv import io import zipfile @@ -11,7 +12,7 @@ from tqdm import tqdm from findthatpostcode import db, settings -from findthatpostcode.documents import Postcode +from findthatpostcode.documents import Postcode, PostcodeSource from findthatpostcode.utils import BulkImporter PC_INDEX = Postcode.Index.name @@ -22,6 +23,64 @@ @click.option("--url", default=settings.NSPL_URL) @click.option("--file", default=None) def import_nspl(url=settings.NSPL_URL, es_index=PC_INDEX, file=None): + return import_from_postcode_file( + url=url, + es_index=es_index, + file=file, + filetype=PostcodeSource.NSPL, + file_location="Data/multi_csv/NSPL", + ) + + +@click.command("onspd") +@click.option("--es-index", default=PC_INDEX) +@click.option("--url", default=settings.ONSPD_URL) +@click.option("--file", default=None) +def import_onspd(url=settings.ONSPD_URL, es_index=PC_INDEX, file=None): + return import_from_postcode_file( + url=url, + es_index=es_index, + file=file, + filetype=PostcodeSource.ONSPD, + file_location="Data/multi_csv/ONSPD", + ) + + +@click.command("nhspd") +@click.option("--es-index", default=PC_INDEX) +@click.option("--url", default=settings.NHSPD_URL) +@click.option("--file", default=None) +def import_nhspd(url=settings.NHSPD_URL, es_index=PC_INDEX, file=None): + return import_from_postcode_file( + url=url, + es_index=es_index, + file=file, + filetype=PostcodeSource.NHSPD, + file_location="Data/", + ) + + +@click.command("pcon") +@click.option("--es-index", default=PC_INDEX) +@click.option("--url", default=settings.PCON_URL) +@click.option("--file", default=None) +def import_pcon(url=settings.PCON_URL, es_index=PC_INDEX, file=None): + return import_from_postcode_file( + url=url, + es_index=es_index, + file=file, + filetype=PostcodeSource.PCON, + file_location="pcd_pcon_", + ) + + +def import_from_postcode_file( + url=settings.NSPL_URL, + es_index=PC_INDEX, + file=None, + filetype: PostcodeSource = PostcodeSource.NSPL, + file_location: str = "Data/multi_csv/NSPL", +): if settings.DEBUG: requests_cache.install_cache() @@ -35,25 +94,35 @@ def import_nspl(url=settings.NSPL_URL, es_index=PC_INDEX, file=None): r = requests.get(url, stream=True) z = zipfile.ZipFile(io.BytesIO(r.content)) + fieldnames = None + if filetype == PostcodeSource.NHSPD: + fieldnames = settings.NHSPD_FIELDNAMES + for f in z.filelist: - if not f.filename.endswith(".csv") or not f.filename.startswith( - "Data/multi_csv/NSPL" - ): + if not f.filename.endswith(".csv") or not f.filename.startswith(file_location): continue print(f"[postcodes] Opening {f.filename}") with z.open(f, "r") as pccsv, BulkImporter(es, name="postcodes") as importer: pccsv = io.TextIOWrapper(pccsv) - reader = csv.DictReader(pccsv) + reader = csv.DictReader(pccsv, fieldnames=fieldnames) for record in tqdm(reader): + if filetype == PostcodeSource.PCON: + record = { + "pcds": record["pcd"], + "pcon25": record["pconcd"], + } + importer.add( { "_index": es_index, "_op_type": "update", "_id": record["pcds"], "doc_as_upsert": True, - "doc": Postcode.from_csv(record).to_dict(), + "doc": { + filetype.value: Postcode.from_csv(record).to_dict(), + }, } ) diff --git a/findthatpostcode/commands/utils.py b/findthatpostcode/commands/utils.py new file mode 100644 index 0000000..2ae9950 --- /dev/null +++ b/findthatpostcode/commands/utils.py @@ -0,0 +1,27 @@ +import random +import zipfile + +import click + + +@click.command("sample-zip") +@click.argument("input", type=click.Path(exists=True)) +@click.argument("output", type=click.Path()) +def sample_zip(input, output): + input_zip = zipfile.ZipFile(input) + output_zip = zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) + + for f in input_zip.filelist: + # if the file is not a CSV then just copy it across + if not f.filename.endswith(".csv"): + output_zip.writestr(f, input_zip.read(f)) + continue + + # if it's a CSV the read it in and write out a sample + with input_zip.open(f, "r") as pccsv: + lines = pccsv.readlines() + if len(lines) <= 1000: + output_lines = lines + else: + output_lines = [lines[0]] + random.sample(lines[1:], 100) + output_zip.writestr(f, b"".join(output_lines)) diff --git a/findthatpostcode/crud.py b/findthatpostcode/crud.py index da36ef3..e19dfb8 100644 --- a/findthatpostcode/crud.py +++ b/findthatpostcode/crud.py @@ -59,8 +59,7 @@ def record_to_schema( name_fields: Optional[List[str]] = None, name_lookup: Optional[Dict[str, Optional[str]]] = None, **kwargs, -) -> schemas.Area: - ... +) -> schemas.Area: ... @overload @@ -70,8 +69,7 @@ def record_to_schema( name_fields: Optional[List[str]] = None, name_lookup: Optional[Dict[str, Optional[str]]] = None, **kwargs, -) -> schemas.Placename: - ... +) -> schemas.Placename: ... @overload @@ -81,8 +79,7 @@ def record_to_schema( name_fields: Optional[List[str]] = None, name_lookup: Optional[Dict[str, Optional[str]]] = None, **kwargs, -) -> schemas.NearestPoint: - ... +) -> schemas.NearestPoint: ... @overload @@ -92,8 +89,7 @@ def record_to_schema( name_fields: Optional[List[str]] = None, name_lookup: Optional[Dict[str, Optional[str]]] = None, **kwargs, -) -> schemas.Postcode: - ... +) -> schemas.Postcode: ... def record_to_schema( diff --git a/findthatpostcode/documents/__init__.py b/findthatpostcode/documents/__init__.py index 688794b..00089a4 100644 --- a/findthatpostcode/documents/__init__.py +++ b/findthatpostcode/documents/__init__.py @@ -1,6 +1,6 @@ from .area import Area from .entity import Entity from .placename import Placename -from .postcode import Postcode +from .postcode import Postcode, PostcodeSource -__all__ = ["Postcode", "Entity", "Area", "Placename"] +__all__ = ["Postcode", "Entity", "Area", "Placename", "PostcodeSource"] diff --git a/findthatpostcode/documents/postcode.py b/findthatpostcode/documents/postcode.py index 14ff564..ad3be03 100644 --- a/findthatpostcode/documents/postcode.py +++ b/findthatpostcode/documents/postcode.py @@ -1,5 +1,6 @@ import datetime import hashlib +from enum import Enum from typing import Any, Dict, List from elasticsearch_dsl import Document, field @@ -8,6 +9,13 @@ from findthatpostcode.utils import PostcodeStr +class PostcodeSource(Enum): + NSPL = "nspl" + ONSPD = "onspd" + NHSPD = "nhspd" + PCON = "pcon" # new parliamentary constituencies - separate lookup provided + + class Postcode(Document): pcd = field.Keyword() pcd2 = field.Keyword() @@ -68,10 +76,14 @@ def area_codes(self) -> List[str]: return [f for f in self.to_dict().values() if isinstance(f, str)] @classmethod - def from_csv(cls, original_record: Dict[str, str]) -> "Postcode": + def from_csv( + cls, + original_record: Dict[str, str], + ) -> "Postcode": """Create a Postcode object from a NSPL record""" record: Dict[str, Any] = original_record.copy() postcode = PostcodeStr(record["pcds"]) + record["pcds"] = str(postcode) # null any blank fields (or ones with a dummy code in) for k in record: @@ -80,24 +92,36 @@ def from_csv(cls, original_record: Dict[str, str]) -> "Postcode": # date fields for date_field in ["dointr", "doterm"]: - if record[date_field]: + if record.get(date_field): record[date_field] = datetime.datetime.strptime( record[date_field], "%Y%m" ) # latitude and longitude for geo_field in ["lat", "long"]: - if record[geo_field]: + if record.get(geo_field): record[geo_field] = float(record[geo_field]) if record[geo_field] == 99.999999: record[geo_field] = None - if record["lat"] and record["long"]: + if record.get("lat") and record.get("long"): record["location"] = {"lat": record["lat"], "lon": record["long"]} # integer fields - for int_field in ["oseast1m", "osnrth1m", "usertype", "osgrdind", "imd"]: - if record[int_field]: - record[int_field] = int(record[int_field]) + for int_field in [ + "oseast1m", + "osnrth1m", + "oseast100m", + "osnrth100m", + "usertype", + "osgrdind", + "imd", + ]: + if record.get(int_field): + value = record[int_field].strip() + if value == "": + record[int_field] = None + else: + record[int_field] = int(value) # add postcode hash record["hash"] = hashlib.md5( diff --git a/findthatpostcode/main.py b/findthatpostcode/main.py index acd8224..7e11786 100644 --- a/findthatpostcode/main.py +++ b/findthatpostcode/main.py @@ -1,14 +1,15 @@ -import datetime from typing import Any from elasticsearch import Elasticsearch from fastapi import Depends, FastAPI, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates +from starlette.routing import Route as StarletteRoute -from findthatpostcode import api, crud, graphql, settings +from findthatpostcode import api, crud, graphql from findthatpostcode.db import get_db +from findthatpostcode.routers import areatypes, postcodes, tools +from findthatpostcode.utils import templates description = """ This site presents data on UK postcodes and geographical areas, based on open data released by @@ -46,7 +47,7 @@ version="1.0.0", # terms_of_service="http://example.com/terms/", contact={ - "name": "David Kane", + "name": "Kane Data Limited", "url": "https://dkane.net/", "email": "info@findthatpostcode.uk", }, @@ -56,26 +57,21 @@ "description": "Postcode lookup", } ], + openapi_url="/api/v1/openapi.json", + docs_url="/api/v1/docs", # license_info={ # "name": "Apache 2.0", # "url": "https://www.apache.org/licenses/LICENSE-2.0.html", # }, ) -templates = Jinja2Templates(directory="templates") -templates.env.globals.update( - dict( - now=datetime.datetime.now(), - key_area_types=settings.KEY_AREA_TYPES, - other_codes=settings.OTHER_CODES, - area_types=settings.AREA_TYPES, - ) -) - app.mount("/static", StaticFiles(directory="static"), name="static") app.include_router(api.router) app.include_router(graphql.router, prefix="/graphql", include_in_schema=False) +app.include_router(tools, prefix="/tools") +app.include_router(postcodes, prefix="/postcodes") +app.include_router(areatypes, prefix="/areatypes") @app.get("/", response_class=HTMLResponse, include_in_schema=False) @@ -95,35 +91,16 @@ def search(request: Request, db: Elasticsearch = Depends(get_db)): return templates.TemplateResponse(request, "areasearch.html.j2", context) -@app.get("/areatypes/", response_class=HTMLResponse, include_in_schema=False) -def all_areatypes(request: Request, db: Elasticsearch = Depends(get_db)): - return templates.TemplateResponse( - request, - "areatypes.html.j2", - {"areatypes": crud.get_all_areatypes(db)}, - ) +@app.get("/addtocsv/", response_class=RedirectResponse, include_in_schema=False) +def addtocsv_redirect(request: Request): + return RedirectResponse(request.url_for("tools_addtocsv"), status_code=302) -@app.get("/areatypes/{areacode}", response_class=HTMLResponse, include_in_schema=False) -def get_areatype(areacode: str, request: Request, db: Elasticsearch = Depends(get_db)): - areatype = settings.AREA_TYPES.get(areacode) - return templates.TemplateResponse( - request, - "areatype.html.j2", - { - "result": { - **areatype, - "attributes": { - **areatype, - "count_areas": 0, - }, - "relationships": {}, - "count_areas": 0, - }, - }, - ) - - -@app.get("/addtocsv/", response_class=HTMLResponse, include_in_schema=False) -def addtocsv(request: Request): - return templates.TemplateResponse(request, "addtocsv.html.j2") +@app.get("/url-list") +def get_all_urls(): + url_list = [ + {"path": route.path, "name": route.name} + for route in app.routes + if isinstance(route, StarletteRoute) + ] + return url_list diff --git a/findthatpostcode/routers/__init__.py b/findthatpostcode/routers/__init__.py new file mode 100644 index 0000000..4a5b1f6 --- /dev/null +++ b/findthatpostcode/routers/__init__.py @@ -0,0 +1,5 @@ +from findthatpostcode.routers.areatypes import router as areatypes +from findthatpostcode.routers.postcodes import router as postcodes +from findthatpostcode.routers.tools import router as tools + +__all__ = ["tools", "postcodes", "areatypes"] diff --git a/findthatpostcode/routers/areas.py b/findthatpostcode/routers/areas.py new file mode 100644 index 0000000..e69de29 diff --git a/findthatpostcode/routers/areatypes.py b/findthatpostcode/routers/areatypes.py new file mode 100644 index 0000000..db058ff --- /dev/null +++ b/findthatpostcode/routers/areatypes.py @@ -0,0 +1,39 @@ +from elasticsearch import Elasticsearch +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from findthatpostcode import crud, settings +from findthatpostcode.db import get_db +from findthatpostcode.schemas import Postcode +from findthatpostcode.utils import templates + +router = APIRouter(include_in_schema=False) + + +@router.get("/", response_class=HTMLResponse, include_in_schema=False) +def all_areatypes(request: Request, db: Elasticsearch = Depends(get_db)): + return templates.TemplateResponse( + request, + "areatypes.html.j2", + {"areatypes": crud.get_all_areatypes(db)}, + ) + + +@router.get("/{areacode}", response_class=HTMLResponse, include_in_schema=False) +def get_areatype(areacode: str, request: Request, db: Elasticsearch = Depends(get_db)): + areatype = settings.AREA_TYPES.get(areacode) + return templates.TemplateResponse( + request, + "areatype.html.j2", + { + "result": { + **areatype, + "attributes": { + **areatype, + "count_areas": 0, + }, + "relationships": {}, + "count_areas": 0, + }, + }, + ) diff --git a/findthatpostcode/routers/postcodes.py b/findthatpostcode/routers/postcodes.py new file mode 100644 index 0000000..4d233b1 --- /dev/null +++ b/findthatpostcode/routers/postcodes.py @@ -0,0 +1,41 @@ +from elasticsearch import Elasticsearch +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from findthatpostcode import crud +from findthatpostcode.db import get_db +from findthatpostcode.schemas import Postcode +from findthatpostcode.utils import templates + +router = APIRouter(include_in_schema=False) + + +@router.get( + "/{postcode}.html", + response_class=HTMLResponse, + include_in_schema=False, +) +def get_postcode_html( + postcode: str, + request: Request, + db: Elasticsearch = Depends(get_db), +): + return templates.TemplateResponse( + request, + "postcode.html.j2", + {"result": crud.get_postcode(db, postcode)}, + ) + + +@router.get("/{postcode}", response_model=Postcode, include_in_schema=False) +@router.get( + "/{postcode}.json", + response_model=Postcode, + include_in_schema=False, +) +def get_postcode_json( + postcode: str, + request: Request, + db: Elasticsearch = Depends(get_db), +): + return crud.get_postcode(db, postcode) diff --git a/findthatpostcode/routers/tools.py b/findthatpostcode/routers/tools.py new file mode 100644 index 0000000..c61dfc5 --- /dev/null +++ b/findthatpostcode/routers/tools.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse + +from findthatpostcode.utils import templates + +router = APIRouter(include_in_schema=False, default_response_class=HTMLResponse) + + +@router.get("/merge-geojson") +def geojson_merge(request: Request): + return templates.TemplateResponse(request, "merge-geojson.html.j2") + + +@router.get("/reduce-geojson") +def geojson_reduce(request: Request): + return templates.TemplateResponse(request, "reduce-geojson.html.j2") + + +@router.get("/addtocsv", name="tools_addtocsv") +def addtocsv(request: Request): + return templates.TemplateResponse(request, "addtocsv.html.j2") diff --git a/findthatpostcode/schemas.py b/findthatpostcode/schemas.py index d6ce54c..dbcb06c 100644 --- a/findthatpostcode/schemas.py +++ b/findthatpostcode/schemas.py @@ -1,5 +1,5 @@ from datetime import date -from typing import List, Optional +from typing import List, Literal, Optional import strawberry from pydantic import BaseModel, Field @@ -7,18 +7,29 @@ from findthatpostcode import settings from findthatpostcode.commands.placenames import PLACE_TYPES +from findthatpostcode.utils import PostcodeStr class HTTPNotFoundError(BaseModel): detail: str +oac11_type = Literal["supergroup", "group", "subgroup"] + + +@strawberry.type(description="Lat Long") +@dataclass +class LatLng: + lat: float + lon: float + + @strawberry.type(description="Postcode") @dataclass class Postcode: - # pcd: str - # pcd2: str pcds: str + pcd: Optional[str] = None + pcd2: Optional[str] = None dointr: Optional[date] = None doterm: Optional[date] = None usertype: Optional[int] = None @@ -34,21 +45,16 @@ class Postcode: laua_name: Optional[str] = Field(None, title="Local Authority (Name)") ward: Optional[str] = Field(None, title="Ward (GSS Code)") ward_name: Optional[str] = Field(None, title="Ward (Name)") - hlthau: Optional[str] = Field(None, title="Health Authority (GSS Code)") - hlthau_name: Optional[str] = Field(None, title="Health Authority (Name)") - nhser: Optional[str] = None + nhser: Optional[str] = Field(None, title="NHS England Region (GSS Code)") + nhser_name: Optional[str] = Field(None, title="NHS England Region (Name)") ctry: Optional[str] = Field(None, title="Country (GSS Code)") ctry_name: Optional[str] = Field(None, title="Country (Name)") rgn: Optional[str] = Field(None, title="Region (GSS Code)") rgn_name: Optional[str] = Field(None, title="Region (Name)") pcon: Optional[str] = Field(None, title="Parliamentary Constituency (GSS Code)") pcon_name: Optional[str] = Field(None, title="Parliamentary Constituency (Name)") - eer: Optional[str] = None - teclec: Optional[str] = None ttwa: Optional[str] = Field(None, title="Travel to Work Area (GSS Code)") ttwa_name: Optional[str] = Field(None, title="Travel to Work Area (Name)") - pct: Optional[str] = Field(None, title="Primary Care Trust (GSS Code)") - pct_name: Optional[str] = Field(None, title="Primary Care Trust (Name)") nuts: Optional[str] = None npark: Optional[str] = Field(None, title="National Park (GSS Code)") npark_name: Optional[str] = Field(None, title="National Park (Name)") @@ -56,11 +62,12 @@ class Postcode: lsoa11_name: Optional[str] = Field(None, title="Lower Super Output Area (Name)") msoa11: Optional[str] = Field(None, title="Middle Super Output Area (GSS Code)") msoa11_name: Optional[str] = Field(None, title="Middle Super Output Area (Name)") - wz11: Optional[str] = None + wz11: Optional[str] = Field(None, title="Workplace Zone (GSS Code)") + wz11_name: Optional[str] = Field(None, title="Workplace Zone (Name)") ccg: Optional[str] = Field(None, title="Clinical Commissioning Group (GSS Code)") ccg_name: Optional[str] = Field(None, title="Clinical Commissioning Group (Name)") - bua11: Optional[str] = None - buasd11: Optional[str] = None + bua11: Optional[str] = Field(None, title="Built up Area (GSS Code)") + bua11_name: Optional[str] = Field(None, title="Built up Area (Name)") ru11ind: Optional[str] = None oac11: Optional[str] = None lat: Optional[float] = Field(None, title="Latitude") @@ -71,15 +78,65 @@ class Postcode: lep2_name: Optional[str] = Field( None, title="Local Enterprise Partnership 2 (Name)" ) - pfa: Optional[str] = None + pfa: Optional[str] = Field(None, title="Police Force Area (GSS Code)") + pfa_name: Optional[str] = Field(None, title="Police Force Area (Name)") imd: Optional[int] = None - calncv: Optional[str] = None stp: Optional[str] = None # hash: Optional[str] = None + location: Optional[LatLng] = None + class Config: orm_mode = True + @property + def pcd_outward(self) -> str: + return PostcodeStr(self.pcds).postcode_district + + @property + def pcd_inward(self) -> str: + return PostcodeStr(self.pcds).inward + + @property + def pcd_area(self) -> str: + return PostcodeStr(self.pcds).postcode_area + + @property + def pcd_district(self) -> str: + # district is another name for outward code + return self.pcd_outward + + @property + def pcd_sector(self) -> str: + return PostcodeStr(self.pcds).postcode_sector + + @property + def id(self) -> str: + return self.pcds + + def get_area(self, areatype) -> Optional["Area"]: + area_code = getattr(self, areatype, None) + if not isinstance(area_code, str): + return None + return Area( + code=area_code, + name=getattr(self, areatype + "_name", area_code), + entity=area_code[0:3], + ) + + def get_oac11(self, oactype: oac11_type) -> Optional[str]: + oac11 = self.oac11 + if not isinstance(oac11, str): + return None + type_index = ["supergroup", "group", "subgroup"].index(oactype) + return settings.OAC11_CODE[oac11][type_index] + + def get_ru11ind_decsription(self) -> Optional[str]: + ru11ind = self.ru11ind + if not isinstance(ru11ind, str): + return None + return settings.RU11IND_CODES.get(ru11ind) + @dataclass class Point: @@ -98,7 +155,7 @@ class Config: @dataclass class Area: code: str - name: str + name: Optional[str] = None name_welsh: Optional[str] = None areachect: Optional[float] = None areaehect: Optional[float] = None diff --git a/findthatpostcode/settings.py b/findthatpostcode/settings.py index adec80a..77c2289 100644 --- a/findthatpostcode/settings.py +++ b/findthatpostcode/settings.py @@ -38,6 +38,8 @@ def get_es_url(default: str) -> str: # postcode data URLs NSPL_URL = "https://www.arcgis.com/sharing/rest/content/items/677cfc3ef56541999314efc795664ce9/data" ONSPD_URL = "https://www.arcgis.com/sharing/rest/content/items/a644dd04d18f4592b7d36705f93270d8/data" +NHSPD_URL = "https://www.arcgis.com/sharing/rest/content/items/c290e7ec05d542e1a38d0822aaf3e634/data" +PCON_URL = "https://www.arcgis.com/sharing/rest/content/items/0ce50b21cd5a4599b6df0452f7fed235/data" # area data URLs RGC_URL = "https://www.arcgis.com/sharing/rest/content/items/7216e9b54a1b49459aaaf59b3f122abc/data" @@ -66,8 +68,8 @@ def get_es_url(default: str) -> str: KEY_AREA_TYPES = [ ("Key", ["ctry", "rgn", "cty", "laua", "ward", "msoa11", "pcon"]), ("Secondary", ["ttwa", "pfa", "lep", "lsoa11", "oa11", "npark"]), - ("Health", ["ccg", "nhser", "hb", "lhb"]), - ("Other", ["eer", "bua11", "buasd11", "wz11", "teclec"]), + ("Health", ["ccg", "nhser", "lhb"]), + ("Other", ["bua11", "wz11"]), ] OTHER_CODES = { @@ -368,3 +370,56 @@ def get_es_url(default: str) -> str: "7": "Remote Rural", "8": "Very Remote Rural", } + + +NHSPD_FIELDNAMES = [ + "pcd2", + "pcds", + "dointr", + "doterm", + "oseast100m", + "osnrth100m", + "oscty", + "odslaua", + "oslaua", + "osward", + "usertype", + "osgrdind", + "ctry", + "oshlthau", + "rgn", + "oldha", + "nhser", + "sicbl", + "psed", + "cened", + "edind", + "ward98", + "oa01", + "nhsrlo", + "hro", + "lsoa01", + "ur01ind", + "msoa01", + "cannet", + "scn", + "oshaprev", + "oldpct", + "oldhro", + "pcon", + "canreg", + "pct", + "oseast1m", + "osnrth1m", + "oa11", + "lsoa11", + "msoa11", + "calncv", + "icb", + "smhpc_aed", + "smhpc_as", + "smhpc_ct4", + "oa21", + "lsoa21", + "msoa21", +] diff --git a/findthatpostcode/tests/fixtures/__init__.py b/findthatpostcode/tests/fixtures/__init__.py index f150ec6..6d98da5 100644 --- a/findthatpostcode/tests/fixtures/__init__.py +++ b/findthatpostcode/tests/fixtures/__init__.py @@ -3,14 +3,25 @@ from fastapi.testclient import TestClient from findthatpostcode.main import app, get_db -from findthatpostcode.settings import CHD_URL, MSOA_URL, NSPL_URL, RGC_URL +from findthatpostcode.settings import ( + CHD_URL, + MSOA_URL, + NHSPD_URL, + NSPL_URL, + ONSPD_URL, + PCON_URL, + RGC_URL, +) MOCK_FILES = { CHD_URL: os.path.join( os.path.dirname(__file__), "chd.zip", ), - MSOA_URL: os.path.join(os.path.dirname(__file__), "msoanames.csv"), + MSOA_URL: os.path.join( + os.path.dirname(__file__), + "msoanames.csv", + ), RGC_URL: os.path.join( os.path.dirname(__file__), "rgc.zip", @@ -19,6 +30,18 @@ os.path.dirname(__file__), "nspl21.zip", ), + ONSPD_URL: os.path.join( + os.path.dirname(__file__), + "onspd.zip", + ), + NHSPD_URL: os.path.join( + os.path.dirname(__file__), + "nhspd.zip", + ), + PCON_URL: os.path.join( + os.path.dirname(__file__), + "pcd_pcon.zip", + ), } diff --git a/findthatpostcode/tests/fixtures/nhspd.zip b/findthatpostcode/tests/fixtures/nhspd.zip new file mode 100644 index 0000000..d33f9f8 Binary files /dev/null and b/findthatpostcode/tests/fixtures/nhspd.zip differ diff --git a/findthatpostcode/tests/fixtures/nsul.zip b/findthatpostcode/tests/fixtures/nsul.zip new file mode 100644 index 0000000..76fe51c Binary files /dev/null and b/findthatpostcode/tests/fixtures/nsul.zip differ diff --git a/findthatpostcode/tests/fixtures/onspd.zip b/findthatpostcode/tests/fixtures/onspd.zip new file mode 100644 index 0000000..0ec1862 Binary files /dev/null and b/findthatpostcode/tests/fixtures/onspd.zip differ diff --git a/findthatpostcode/tests/fixtures/onsud.zip b/findthatpostcode/tests/fixtures/onsud.zip new file mode 100644 index 0000000..529e31e Binary files /dev/null and b/findthatpostcode/tests/fixtures/onsud.zip differ diff --git a/findthatpostcode/tests/fixtures/pcd_pcon.zip b/findthatpostcode/tests/fixtures/pcd_pcon.zip new file mode 100644 index 0000000..a82d422 Binary files /dev/null and b/findthatpostcode/tests/fixtures/pcd_pcon.zip differ diff --git a/findthatpostcode/tests/test_commands/test_postcodes.py b/findthatpostcode/tests/test_commands/test_postcodes.py index d459cde..34440c1 100644 --- a/findthatpostcode/tests/test_commands/test_postcodes.py +++ b/findthatpostcode/tests/test_commands/test_postcodes.py @@ -1,26 +1,45 @@ +import pytest from click.testing import CliRunner import findthatpostcode.utils -from findthatpostcode.commands.postcodes import Postcode, db, import_nspl -from findthatpostcode.settings import NSPL_URL +from findthatpostcode.commands.postcodes import ( + Postcode, + db, + import_nhspd, + import_nspl, + import_onspd, + import_pcon, +) +from findthatpostcode.settings import NHSPD_URL, NSPL_URL, ONSPD_URL, PCON_URL from findthatpostcode.tests.fixtures import MOCK_FILES, MockES, mock_bulk +files = [ + ("nspl", NSPL_URL, import_nspl), + ("onspd", ONSPD_URL, import_onspd), + ("nhspd", NHSPD_URL, import_nhspd), + ("pcon", PCON_URL, import_pcon), +] -def test_import_nspl(requests_mock, monkeypatch): +parameters = [] +for f in files: + url = f[1] + parameters.append(tuple([*f, ["--file", MOCK_FILES[url]]])) + parameters.append(tuple([*f, []])) + parameters.append(tuple([*f, ["--url", url]])) + + +@pytest.mark.parametrize("filetype, url, command, args", parameters) +def test_import_postcode(filetype, url, command, args, requests_mock, monkeypatch): with open( - MOCK_FILES[NSPL_URL], + MOCK_FILES[url], "rb", ) as f: - requests_mock.get(NSPL_URL, content=f.read()) + requests_mock.get(url, content=f.read()) monkeypatch.setattr(db, "get_db", lambda: MockES()) monkeypatch.setattr(Postcode, "init", lambda *args, **kwargs: None) monkeypatch.setattr(findthatpostcode.utils, "bulk", mock_bulk) runner = CliRunner() - result = runner.invoke(import_nspl, catch_exceptions=False) - assert result.exit_code == 0 - result = runner.invoke( - import_nspl, ["--file", MOCK_FILES[NSPL_URL]], catch_exceptions=False - ) + result = runner.invoke(command, args, catch_exceptions=False) assert result.exit_code == 0 diff --git a/findthatpostcode/tests/test_pages.py b/findthatpostcode/tests/test_pages.py index 7d8a766..6bfce45 100644 --- a/findthatpostcode/tests/test_pages.py +++ b/findthatpostcode/tests/test_pages.py @@ -81,13 +81,19 @@ def test_areatypes_html(): assert "Browse by area type" in response.text +def test_addtocsv_redirect(): + response = client.get("/addtocsv/", follow_redirects=False) + assert response.status_code == 302 + assert response.headers["location"] == str(client.base_url) + "/tools/addtocsv" + + def test_addtocsv(): - response = client.get("/addtocsv/") + response = client.get("/tools/addtocsv") assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" assert "Add fields to CSV" in response.text assert ( - "Therefore it is recommended that you think carefully before using this tool with any personal or sensitive data." + "think carefully before using this tool with any personal or sensitive data." in response.text ) diff --git a/findthatpostcode/utils.py b/findthatpostcode/utils.py index 782a85c..bde6e78 100644 --- a/findthatpostcode/utils.py +++ b/findthatpostcode/utils.py @@ -4,11 +4,14 @@ from typing import Any, Dict, Optional from elasticsearch.helpers import bulk +from fastapi.templating import Jinja2Templates + +from findthatpostcode import settings LATLONG_REGEX = re.compile(r"^(?P\-?\d+\.[0-9]+),(?P\-?\d+\.\d+)$") POSTCODE_REGEX = re.compile(r"^[A-Z]{1,2}[0-9][0-9A-Z]? ?[0-9][A-Z]{2}$") POSTCODE_REGEX = re.compile( - r"^(([A-Z]{1,2}[0-9][A-Z0-9]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?[0-9][A-Z]{2}|BFPO ?[0-9]{1,4}|(KY[0-9]|MSR|VG|AI)[ -]?[0-9]{4}|[A-Z]{2} ?[0-9]{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$" + r"^(([A-Z]{1,2}[0-9][A-Z0-9]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA|NPT) ?[0-9][A-Z]{2}|BFPO ?[0-9]{1,4}|(KY[0-9]|MSR|VG|AI)[ -]?[0-9]{4}|[A-Z]{2} ?[0-9]{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$" ) POSTCODE_AREAS = { @@ -318,3 +321,22 @@ def postcode_sector(self) -> str: 'SW1A 1' """ return self.postcode[:-2] + + @property + def outward(self) -> str: + return self._outward_code + + @property + def inward(self) -> str: + return self._inward_code + + +templates = Jinja2Templates(directory="templates") +templates.env.globals.update( + dict( + now=datetime.datetime.now(), + key_area_types=settings.KEY_AREA_TYPES, + other_codes=settings.OTHER_CODES, + area_types=settings.AREA_TYPES, + ) +) diff --git a/readme.md b/readme.md index 73b1cfe..584e563 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ Run a development server ``` -uvicorn findthatpostcode:app --debug --reload +uvicorn findthatpostcode:app --reload ``` ## Testing diff --git a/requirements.txt b/requirements.txt index 4cb3ffc..0ddee8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,9 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile -# +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in alembic==1.13.1 - # via -r requirements.in annotated-types==0.6.0 # via pydantic -anyio==4.2.0 +anyio==4.3.0 # via # httpx # starlette @@ -17,19 +12,17 @@ attrs==23.2.0 # via # cattrs # requests-cache -boto3==1.34.18 - # via -r requirements.in -boto3-stubs[s3]==1.34.18 - # via -r requirements.in -botocore==1.34.18 +boto3==1.34.93 +boto3-stubs==1.34.93 +botocore==1.34.93 # via # boto3 # s3transfer -botocore-stubs==1.34.18 +botocore-stubs==1.34.93 # via boto3-stubs cattrs==23.2.3 # via requests-cache -certifi==2023.11.17 +certifi==2024.2.2 # via # elasticsearch # httpcore @@ -38,32 +31,25 @@ certifi==2023.11.17 charset-normalizer==3.3.2 # via requests click==8.1.7 - # via - # -r requirements.in - # uvicorn + # via uvicorn colorama==0.4.6 # via # click # pytest # tqdm # uvicorn -coverage==7.4.0 - # via -r requirements.in +coverage==7.5.0 elasticsearch==7.9.1 # via elasticsearch-dsl elasticsearch-dsl==7.4.1 - # via -r requirements.in -exceptiongroup==1.2.0 +exceptiongroup==1.2.1 # via # anyio # cattrs # pytest -fastapi==0.109.0 - # via - # -r requirements.in - # strawberry-graphql -geoalchemy2==0.14.3 - # via -r requirements.in +fastapi==0.110.2 + # via strawberry-graphql +geoalchemy2==0.15.0 graphql-core==3.2.3 # via strawberry-graphql greenlet==3.0.3 @@ -72,13 +58,12 @@ h11==0.14.0 # via # httpcore # uvicorn -httpcore==1.0.2 +httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn -httpx==0.26.0 - # via -r requirements.in -idna==3.6 +httpx==0.27.0 +idna==3.7 # via # anyio # httpx @@ -86,107 +71,90 @@ idna==3.6 iniconfig==2.0.0 # via pytest jinja2==3.1.3 - # via -r requirements.in jmespath==1.0.1 # via # boto3 # botocore -mako==1.3.0 +mako==1.3.3 # via alembic -markupsafe==2.1.3 +markupsafe==2.1.5 # via # jinja2 # mako -mypy==1.8.0 +mypy==1.10.0 # via pydantic-geojson -mypy-boto3-s3==1.34.14 +mypy-boto3-s3==1.34.91 # via boto3-stubs mypy-extensions==1.0.0 # via mypy -numpy==1.26.3 +numpy==1.26.4 # via shapely -packaging==23.2 +packaging==24.0 # via # geoalchemy2 # pytest -platformdirs==4.1.0 +platformdirs==4.2.1 # via requests-cache -pluggy==1.3.0 +pluggy==1.5.0 # via pytest psycopg2-binary==2.9.9 - # via -r requirements.in -pydantic==2.5.3 +pydantic==2.7.1 # via # fastapi # pydantic-geojson -pydantic-core==2.14.6 +pydantic-core==2.18.2 # via pydantic pydantic-geojson==0.1.1 - # via -r requirements.in -pytest==7.4.4 - # via - # -r requirements.in - # pytest-mock -pytest-mock==3.12.0 - # via -r requirements.in -python-dateutil==2.8.2 +pytest==8.2.0 + # via pytest-mock +pytest-mock==3.14.0 +python-dateutil==2.9.0.post0 # via # botocore # elasticsearch-dsl # strawberry-graphql -python-dotenv==1.0.0 - # via - # -r requirements.in - # uvicorn -python-multipart==0.0.6 +python-dotenv==1.0.1 + # via uvicorn +python-multipart==0.0.9 # via strawberry-graphql pyyaml==6.0.1 # via uvicorn requests==2.31.0 # via - # -r requirements.in # requests-cache # requests-mock -requests-cache==1.1.1 - # via -r requirements.in -requests-mock==1.11.0 - # via -r requirements.in -ruff==0.1.13 - # via -r requirements.in -s3transfer==0.10.0 +requests-cache==1.2.0 +requests-mock==1.12.1 +ruff==0.4.2 +s3transfer==0.10.1 # via boto3 -shapely==2.0.2 - # via -r requirements.in +shapely==2.0.4 six==1.16.0 # via # elasticsearch-dsl # python-dateutil - # requests-mock # url-normalize -sniffio==1.3.0 +sniffio==1.3.1 # via # anyio # httpx -sqlalchemy==2.0.25 +sqlalchemy==2.0.29 # via - # -r requirements.in # alembic # geoalchemy2 -starlette==0.35.1 +starlette==0.37.2 # via fastapi -strawberry-graphql[fastapi]==0.217.1 - # via -r requirements.in +strawberry-graphql==0.227.2 tomli==2.0.1 # via # mypy # pytest -tqdm==4.66.1 - # via -r requirements.in -types-awscrt==0.20.0 +tqdm==4.66.2 +types-awscrt==0.20.9 # via botocore-stubs -types-s3transfer==0.10.0 +types-s3transfer==0.10.1 # via boto3-stubs -typing-extensions==4.9.0 +typing-extensions==4.11.0 # via # alembic # anyio @@ -202,14 +170,13 @@ typing-extensions==4.9.0 # uvicorn url-normalize==1.4.3 # via requests-cache -urllib3==2.0.7 +urllib3==2.2.1 # via # botocore # elasticsearch # requests # requests-cache -uvicorn[standard]==0.25.0 - # via -r requirements.in +uvicorn==0.29.0 watchfiles==0.21.0 # via uvicorn websockets==12.0 diff --git a/templates/_utils.html.j2 b/templates/_utils.html.j2 index e20142c..49be688 100644 --- a/templates/_utils.html.j2 +++ b/templates/_utils.html.j2 @@ -1,35 +1,37 @@ {% macro display_area(area, hide_areatype, div_class='', request=none) %} - {% if area.__class__.__name__ == 'Place' %} - {% set url_source = 'places.get_place' %} - {% else %} - {% set url_source = 'areas.get_area' %} - {% endif %} +{% if area.__class__.__name__ == 'Place' %} +{% set url_source = 'places.get_place' %} +{% else %} +{% set url_source = 'areas.get_area' %} +{% endif %}
- - {# #} + {{- area.code -}} - {# #} - - {% if area.name %} - {# #} - {{- area.name -}} - {# #} - {% endif %} - {% if area.active == false %} - INACTIVE - {% endif %} - {% if area.relationships and area.relationships.areatype and not hide_areatype %} - {% if area.name %} - - ({{- area.relationships.areatype.get_name(area.id) -}}) - - {% else %} - - {{- area.relationships.areatype.get_name(area.id) -}} - - {% endif %} - {% endif %} + + {% if area.name and area.name != area.code %} + + {{- area.name -}} + + {% endif %} + {% if area.active == false %} + INACTIVE + {% endif %} + {% if area.get_areatype() and not hide_areatype %} + {% if area.name %} + + ({{- area.get_areatype().name -}}) + + {% else %} + + {{- area.get_areatype().name -}} + + {% endif %} + {% endif %}
{% endmacro %} @@ -44,9 +46,9 @@ {% macro area_loop(areas, main_id) %} {% endmacro %} diff --git a/templates/addtocsv.html.j2 b/templates/addtocsv.html.j2 index 19df909..6ce424c 100644 --- a/templates/addtocsv.html.j2 +++ b/templates/addtocsv.html.j2 @@ -13,14 +13,14 @@

Add geographical data to a CSV file by looking it up from a postcode column

The file should be separated by commas (,) - not semicolons or tabs) - and the first row should contain the column names.

+ and the first row should contain the column names.

See important note on privacy below

- Step 1: + Step 1: Select CSV file

@@ -51,7 +51,7 @@

- Step 2: + Step 2: Select postcode field

@@ -68,7 +68,7 @@

- Step 3: + Step 3: Select data to add

@@ -79,24 +79,20 @@ Basic fields - Code - Name + Code + + Name + {% for b in basic_fields %} {{b[1]}} - + {% if b[2] %} - + {% endif %} @@ -109,16 +105,13 @@ {% for b in stats_fields %} {{b[1]}} - + {% endfor %} - {% for i in key_area_types %} + {% for i in key_area_types %} {{ i[0] }} areas @@ -132,29 +125,25 @@ [{{ "{:,.0f}".format(result.get(area)) }}] {% endif %} #} - + + {% if "%s_name" % area in default_fields %} checked="checked" {% endif %}> {% endfor %} - {% endfor %} + {% endfor %}
- +
- +

Creating fileā€¦

-
+
+
@@ -176,7 +165,8 @@ and all postcodes that match this code are sent back to the browser to be matched.

- This provides some level of privacy protection, but in some circumstances it could still be possible to make a reasonable guess + This provides some level of privacy protection, but in some circumstances it could still be possible to make a + reasonable guess as to the postcodes used in the file by looking at the codes requested.

Therefore it is recommended @@ -187,7 +177,7 @@ {% block bodyscripts %} {% endblock %} \ No newline at end of file diff --git a/templates/index.html.j2 b/templates/index.html.j2 index 054442b..0b711da 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -23,26 +23,34 @@ {% extends "base.html.j2" %} {% block content %}

- + {% call frontpageblock("Search", title_link=url_for('search'), div_classes='mb6') %} {% include "_search.html.j2" %} {% endcall %} - - + + {% call frontpageblock("Tools", div_classes='w-50-l') %}

- Add data to a CSV with a column of postcode data. + Add data to a CSV with a column of + postcode + data. Information includes latitude/longitude and areas.

- {# Combine GeoJSON files into one merged file. #} + Combine GeoJSON files into one merged + file. +

+

+ Reduce GeoJSON filesize.

{% endcall %} {% call frontpageblock("Use the API", title_link="#api", div_classes='w-50-l') %}

The API provides programmatic access to the postcode data, using the - JSON api specification.


- Use the API + JSON api specification. +


+ Use the API {% endcall %}
@@ -59,18 +67,18 @@ {% block bodyscripts %} {% endblock %} \ No newline at end of file diff --git a/templates/merge-geojson.html.j2 b/templates/merge-geojson.html.j2 index 8c54e63..77eb9d6 100644 --- a/templates/merge-geojson.html.j2 +++ b/templates/merge-geojson.html.j2 @@ -7,7 +7,8 @@

This tool can combine two or more GeoJSON files into one merged file.

-

Choose two or more files from your computer and then click "Download merged file" to save the results.

+

Choose two or more files from your computer and then click "Download merged file" to save the + results.

The tool only accept valid GeoJSON files with a type of FeatureCollection. All features will be merged into @@ -18,9 +19,9 @@

Your file will not leave your own computer and no data from it is sent to Find that Postcode.

- +
- +
<% f.name %><% f.name %> - +

Add more files to create a merged GeoJSON file..

-

Merged GeoJSON file will have <% result.features.length %> features from <% files.length %> files.

+

Merged GeoJSON file will have <% result.features.length %> features from + <% files.length %> files.

Add two or more GeoJSON files to merge

- +
- + - +
{% endblock %} {% block headscripts %} - + {% endblock %} {% block bodyscripts %} - - - + + + {% endblock %} \ No newline at end of file diff --git a/templates/postcode.html.j2 b/templates/postcode.html.j2 index 3486d96..bbe1b23 100644 --- a/templates/postcode.html.j2 +++ b/templates/postcode.html.j2 @@ -1,17 +1,17 @@ {% from '_utils.html.j2' import info_block, display_area, area_loop %} {% if point %} - {% set title = 'Point {:,.5f}, {:,.5f}'.format(*point.id) %} - {% set subtitle = 'Closest postcode to point {0:,.5f}, {1:,.5f} is {3}.
Centre of {3} is {4:,.1f} meters away.'.format( +{% set title = 'Point {:,.5f}, {:,.5f}'.format(*point.id) %} +{% set subtitle = 'Closest postcode to point {0:,.5f}, {1:,.5f} is {3}.
Centre of {3} is {4:,.1f} meters away.'.format( point.id[0], point.id[1], url_for('postcodes.get_postcode', postcode=result.id, filetype='html'), result.id, - point.attributes.distance_from_postcode + point.distance_from_postcode ) %} {% else %} - {% set title = 'Postcode {}'.format(result.id) %} - {% set subtitle = 'No longer used' if result.attributes.doterm else '' %} +{% set title = 'Postcode {}'.format(result.id) %} +{% set subtitle = 'No longer used' if result.doterm else '' %} {% endif %} {% extends 'base.html.j2' %} @@ -24,38 +24,41 @@
- {% if result.attributes.location %} + {% if result.location %} {% call info_block("Latitude and longitude") %} - - {{ result.attributes.location.lat }}, {{ result.attributes.location.lon }} + + {{ result.location.lat }}, {{ result.location.lon }} {% endcall %} {% endif %} {% for i in key_area_types[0:1] %} -

{{ i[0] }} areas

- {% for key_area in i[1] %} - {% set a = result.get_area(key_area) %} - {% if a %} - {% call info_block(a.relationships.areatype.get_name(a.id)) %} - {{ display_area(a, hide_areatype=true) }} - {% endcall %} - {% endif %} - {% endfor %} +

{{ i[0] }} areas

+ {% for key_area in i[1] %} + {% set a = result.get_area(key_area) %} + {% if a %} + {% call info_block(a.get_areatype().name) %} + {{ display_area(a, hide_areatype=true) }} + {% endcall %} + {% endif %} + {% endfor %} {% endfor %}
- {% if result.attributes.location %} + {% if result.location %}
-
+
+
+
{% if result.get_area('laua') %}

- Location within - - {{- result.get_area('laua').attributes.name -}} + Location within + + {{- result.get_area('laua').name -}} Local Authority

@@ -67,82 +70,98 @@

Nearby places

- {% for i in result.relationships.nearest_places %} + {#% for i in result.relationships.nearest_places %}
{{ display_area(i) }}
- {% endfor %} + {% endfor %#}

Area classifications

- {% if result.attributes.oac11 %} + {% if result.oac11 %} {% call info_block("Output area classification") %} - {{ result.attributes.oac11.supergroup }} > {{ result.attributes.oac11.code[0] }}
- {{ result.attributes.oac11.group }} > {{ result.attributes.oac11.code[0:2] }}
- {{ result.attributes.oac11.subgroup }} {{ result.attributes.oac11.code }} + {{ result.get_oac11("supergroup") }} > {{ result.oac11[0] }}
+ {{ result.get_oac11("group") }} > {{ result.oac11[0:2] }}
+ {{ result.get_oac11("subgroup") }} {{ result.oac11 }} {% endcall %} {% endif %} - - {% if result.attributes.ru11ind %} + + {% if result.ru11ind %} {% call info_block("Rural/urban classification") %} - {{ result.attributes.ru11ind.description }} {{ result.attributes.ru11ind.code }} + {{ result.get_ru11ind_decsription() }} {{ result.ru11ind }} {% endcall %} {% endif %} - {% for a in result.relationships.areas if a.attributes.stats and a.attributes.stats.imd2019 %} - {% set rank = a.attributes.stats.imd2019.imd_rank %} - {% set total = other_codes.imd[result.attributes.ctry] %} + {#% for a in result.relationships.areas if a.stats and a.stats.imd2019 %} + {% set rank = a.stats.imd2019.imd_rank %} + {% set total = other_codes.imd[result.ctry] %} {% call info_block("Index of multiple deprivation (2019)") %} -

{{ "{:,.0f}".format(rank) }} out of {{ "{:,.0f}".format(total) }} lower super output areas in {{ result.attributes.ctry_name }} (where 1 is the most deprived LSOA).

-

{{ "{:.0%}".format( rank|float / total|float ) }} of LSOAs in {{ result.attributes.ctry_name }} are more deprived than this one.

- {% if a.attributes.stats.imd2015 %} -

In 2015 {{ "{:.0%}".format( a.attributes.stats.imd2015.imd_rank|float / total|float ) }} of LSOAs in {{ result.attributes.ctry.name }} were more deprived than this one.

- {% endif %} +

{{ "{:,.0f}".format(rank) }} out of {{ "{:,.0f}".format(total) }} lower super output areas in + {{ result.ctry_name }} (where 1 is the most deprived LSOA).

+

{{ "{:.0%}".format( rank|float / total|float ) }} of LSOAs in {{ result.ctry_name }} are more + deprived than this one.

+ {% if a.stats.imd2015 %} +

In 2015 {{ "{:.0%}".format( a.stats.imd2015.imd_rank|float / total|float ) }} of LSOAs + in {{ result.ctry.name }} were more deprived than this one.

+ {% endif %} {% endcall %} - {% endfor %} + {% endfor %#}
{% for i in key_area_types[1:] %}
-

{{ i[0] }} areas

- {% for key_area in i[1] %} - {% set a = result.get_area(key_area) %} - {% if a %} - {% call info_block(a.relationships.areatype.get_name(a.id)) %} - {{ display_area(a, hide_areatype=true) }} - {% endcall %} - {% endif %} - {% endfor %} -
+

{{ i[0] }} areas

+ {% for key_area in i[1] %} + {% set a = result.get_area(key_area) %} + {% if a %} + {% call info_block(a.get_areatype().name) %} + {{ display_area(a, hide_areatype=true) }} + {% endcall %} + {% endif %} + {% endfor %} +
{% endfor %}

Technical details

+ {% call info_block("Postcode area") %} + {{ result.pcd_area }} + {% endcall %} + {% call info_block("Postcode district") %} +

Also known as the outward code

+ {{ result.pcd_outward }} + {% endcall %} + {% call info_block("Inward code") %} + {{ result.pcd_inward }} + {% endcall %} + {% call info_block("Postcode sector") %} + {{ result.pcd_sector }} + {% endcall %} {% call info_block("7 character version of postcode") %} - {{ result.attributes.pcd }} + {{ result.pcd }} {% endcall %} {% call info_block("8 character version of postcode") %} - {{ result.attributes.pcd2 }} + {{ result.pcd2 }} {% endcall %} {% call info_block("Date introduced") %} - {{ "{:%B %Y}".format(result.attributes.dointr) }} + {{ "{:%B %Y}".format(result.dointr) }} {% endcall %} - {% if result.attributes.doterm %} + {% if result.doterm %} {% call info_block("Date terminated") %} - {{ "{:%B %Y}".format(result.attributes.doterm) }} + {{ "{:%B %Y}".format(result.doterm) }} {% endcall %} {% endif %} {% call info_block("Postcode user type") %} - {{result.attributes.usertype }} - {{ other_codes.usertype[result.attributes.usertype] }} + {{result.usertype }} - {{ other_codes.usertype[result.usertype] }} {% endcall %} {% call info_block("Grid reference positional quality indicator") %} - {{result.attributes.osgrdind}} - {{ other_codes.osgrdind[result.attributes.osgrdind] }} + {{result.osgrdind}} - {{ other_codes.osgrdind[result.osgrdind] }} {% endcall %} - {% if result.attributes.oseast1m and result.attributes.osnrth1m %} + {% if result.oseast1m and result.osnrth1m %} {% call info_block("OS Easting/Northing") %} - - {{ result.attributes.oseast1m }}, {{ result.attributes.osnrth1m }} + + {{ result.oseast1m }}, {{ result.osnrth1m }} {% endcall %} {% endif %} @@ -153,19 +172,20 @@
{% if result.id.startswith("BT") %}

- Northern Ireland postcodes are included based on the - Northern Ireland End User Licence. - The licence covers internal use of the data. Commercial use may require additional permission. + Northern Ireland postcodes are included based on the + Northern Ireland End + User Licence. + The licence covers internal use of the data. Commercial use may require additional permission.

{% endif %} {% endblock %} {% block bodyscripts %} -{% if result.attributes.location %} +{% if result.location %} {% endif %} diff --git a/templates/reduce-geojson.html.j2 b/templates/reduce-geojson.html.j2 new file mode 100644 index 0000000..aba1472 --- /dev/null +++ b/templates/reduce-geojson.html.j2 @@ -0,0 +1,216 @@ +{% from '_utils.html.j2' import info_block %} +{% set title = 'Reduce GeoJSON file size' %} +{% extends "base.html.j2" %} + +{% block content %} +
+
+
+

This tool can reduce the file size of your GeoJSON files.

+

It works by rounding the latitude and longitude values in your file. + Values are often given to 10-20 decimal places, but accuracy of 5 decimal places is sufficient for most use + cases.

+

Choose a file from your computer and then click "Reduce file size" to save the results.

+

+ The tool only accept valid GeoJSON + files with a type of FeatureCollection. +

+
+

Privacy

+

Your file will not leave your own computer and no data from it is sent to Find that Postcode.

+
+
+
+

Input file

+
    +
  • File name: <% file.name %>
  • +
  • Features in file: <% file.geojson.features.length %> + features
  • +
  • + Errors: +
      +
    • <% e %>
    • +
    +
  • +
  • Maximum precision: <% precision %> decimal places.
  • +
  • Original file size: <% file.size %> bytes
  • +
  • Estimated reduction: <% estimatedReduction %>%
  • +
+
+ + +
+
+ + + + +
+ A webcomic showing the usefulness of different precision coordinates +
+ Image credit: xkcd 2170. + Used under CC BY-NC 2.5 + License +
+
+
+
+{% endblock %} + +{% block headscripts %} + +{% endblock %} + +{% block bodyscripts %} + + + + +{% endblock %} \ No newline at end of file