Display official planning data.
This commit is contained in:
parent
2bdbbce8d2
commit
8cbb3cd84b
@ -313,6 +313,28 @@
|
|||||||
<PolygonSymbolizer fill="#73ebaf" />
|
<PolygonSymbolizer fill="#73ebaf" />
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style name="planning_applications_status">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Submitted"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#00ffff"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Approved"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#00ff00"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Appeal In Progress"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#ffff00"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Refused"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#ff0000"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Withdrawn"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#999999"/>
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
<Style name="planning_combined">
|
<Style name="planning_combined">
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[planning_in_conservation_area] = true</Filter>
|
<Filter>[planning_in_conservation_area] = true</Filter>
|
||||||
|
@ -116,6 +116,23 @@ const getBuildingUPRNsById = asyncController(async (req: express.Request, res: e
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET building planning data
|
||||||
|
const getBuildingPlanningDataById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
|
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await buildingService.getBuildingPlanningDataById(buildingId);
|
||||||
|
|
||||||
|
if (typeof (result) === 'undefined') {
|
||||||
|
return res.send({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
res.send({data: result, buildingId: buildingId});
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.send({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const getBuildingUserAttributesById = asyncController(async (req: express.Request, res: express.Response) => {
|
const getBuildingUserAttributesById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if(!req.session.user_id) {
|
if(!req.session.user_id) {
|
||||||
return res.send({ error: 'Must be logged in'});
|
return res.send({ error: 'Must be logged in'});
|
||||||
@ -202,6 +219,7 @@ export default {
|
|||||||
getBuildingById,
|
getBuildingById,
|
||||||
updateBuildingById,
|
updateBuildingById,
|
||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
|
getBuildingPlanningDataById,
|
||||||
getUserVerifiedAttributes,
|
getUserVerifiedAttributes,
|
||||||
verifyBuildingAttributes,
|
verifyBuildingAttributes,
|
||||||
getBuildingEditHistoryById,
|
getBuildingEditHistoryById,
|
||||||
|
@ -26,6 +26,7 @@ router.route('/:building_id.json')
|
|||||||
// GET building UPRNs
|
// GET building UPRNs
|
||||||
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
|
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
|
||||||
|
|
||||||
|
router.get('/:building_id/planning_data.json', buildingController.getBuildingPlanningDataById);
|
||||||
|
|
||||||
// POST verify building attribute
|
// POST verify building attribute
|
||||||
router.route('/:building_id/verify.json')
|
router.route('/:building_id/verify.json')
|
||||||
|
@ -56,4 +56,5 @@ export * from './edit';
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './query';
|
export * from './query';
|
||||||
export * from './uprn';
|
export * from './uprn';
|
||||||
|
export * from './planningData';
|
||||||
export * from './verify';
|
export * from './verify';
|
||||||
|
16
app/src/api/services/building/planningData.ts
Normal file
16
app/src/api/services/building/planningData.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import db from '../../../db';
|
||||||
|
|
||||||
|
export async function getBuildingPlanningDataById(id: number) {
|
||||||
|
try {
|
||||||
|
return await db.any(
|
||||||
|
'SELECT building_properties.uprn, building_properties.building_id, planning_data.description, planning_data.status, planning_data.uprn, planning_data.planning_application_id, planning_application_link, to_char(planning_data.decision_date, \'YYYY-MM-DD\') AS decision_date, to_char(planning_data.last_synced_date, \'YYYY-MM-DD\') AS last_synced_date, planning_data.data_source, planning_data.data_source_link \
|
||||||
|
FROM building_properties \
|
||||||
|
INNER JOIN planning_data ON \
|
||||||
|
building_properties.uprn = planning_data.uprn WHERE building_id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -21,12 +21,14 @@ export function useBuildingData(buildingId: number, preloadedData: Building, inc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let [building, buildingUprns] = await Promise.all([
|
let [building, buildingUprns, planningData] = await Promise.all([
|
||||||
apiGet(`/api/buildings/${buildingId}.json${includeUserAttributes ? '?user_attributes=true' : ''}`),
|
apiGet(`/api/buildings/${buildingId}.json${includeUserAttributes ? '?user_attributes=true' : ''}`),
|
||||||
apiGet(`/api/buildings/${buildingId}/uprns.json`)
|
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||||
|
apiGet(`/api/buildings/${buildingId}/planning_data.json`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
building.uprns = buildingUprns.uprns;
|
building.uprns = buildingUprns.uprns;
|
||||||
|
building.planning_data = planningData.data; // TODO use planningData?
|
||||||
building = Object.assign(building, {...building.user_attributes});
|
building = Object.assign(building, {...building.user_attributes});
|
||||||
delete building.user_attributes;
|
delete building.user_attributes;
|
||||||
|
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
import DataTitle from './data-title';
|
||||||
|
import InfoBox from '../../components/info-box';
|
||||||
|
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||||
|
|
||||||
|
|
||||||
|
interface PlanningDataOfficialDataEntryProps {
|
||||||
|
value: any; // TODO: proper structuring!
|
||||||
|
}
|
||||||
|
|
||||||
|
const {useState} = React;
|
||||||
|
|
||||||
|
const LongText = ({ content,limit}) => {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
const showMore = () => setShowAll(true);
|
||||||
|
const showLess = () => setShowAll(false);
|
||||||
|
|
||||||
|
if (content.length <= limit) {
|
||||||
|
return <div>{content}</div>
|
||||||
|
}
|
||||||
|
if (showAll) {
|
||||||
|
return <div>
|
||||||
|
{content}
|
||||||
|
<b onClick={showLess}>shorten description</b>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
const toShow = content.substring(0, limit).trim() + "... ";
|
||||||
|
return <div>
|
||||||
|
{toShow}
|
||||||
|
<b onClick={showMore}>show full description</b>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanningDataOfficialDataEntry: React.FC<PlanningDataOfficialDataEntryProps> = (props) => {
|
||||||
|
|
||||||
|
const data = props.value || [];
|
||||||
|
if(data.length == 0) {
|
||||||
|
return (<Fragment>
|
||||||
|
<InfoBox type='success'>
|
||||||
|
<DataTitle
|
||||||
|
title={"Planning Application Status"}
|
||||||
|
tooltip={null}
|
||||||
|
/>
|
||||||
|
<div>Disclaimer: data is imported from the official source, but Planning London DataHub is known to be incomplete.</div>
|
||||||
|
<b>No live planning data available currently for this building polygon via the Planning London DataHub.</b>
|
||||||
|
</InfoBox>
|
||||||
|
</Fragment>);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<InfoBox type='success'>
|
||||||
|
<Fragment>
|
||||||
|
<DataTitle
|
||||||
|
title={"Planning Application Status"}
|
||||||
|
tooltip={null}
|
||||||
|
/>
|
||||||
|
<div>Disclaimer: data is imported from the official source, but Planning London DataHub is known to be incomplete.</div>
|
||||||
|
<b>Data source:</b> <a href={data[0]["data_source_link"]}>{data[0]["data_source"]}</a>
|
||||||
|
<br/>
|
||||||
|
<b>Planning application ID:</b> {data[0]["planning_application_id"]}
|
||||||
|
<br/>
|
||||||
|
<b>Most recent update by data provider:</b> {data[0]["last_synced_date"]}
|
||||||
|
<br/>
|
||||||
|
<b>Planning application ID:</b> {data[0]["planning_application_id"]}
|
||||||
|
<br/>
|
||||||
|
<b>Current planning application status for this site:</b> {data[0]["status"]}
|
||||||
|
<br/>
|
||||||
|
<b>Decision date</b>: {data[0]["decision_date"].toString()}
|
||||||
|
<br/>
|
||||||
|
<b>Brief Description of proposed work</b>: <LongText content = {data[0]["description"]} limit = {400}/>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<CheckboxDataEntry
|
||||||
|
title="Show conservation area layer (Ian Hall dataset)"
|
||||||
|
slug="planning_recent_outcome"
|
||||||
|
value={null}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
</InfoBox>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlanningDataOfficialDataEntry;
|
@ -8,17 +8,18 @@ import { DataEntryGroup } from '../data-components/data-entry-group';
|
|||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
import Verification from '../data-components/verification';
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
import PlanningDataOfficialDataEntry from '../data-components/planning-data-entry';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
|
|
||||||
/**
|
|
||||||
* Planning view/edit section
|
|
||||||
*/
|
|
||||||
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InfoBox type='warning'>
|
<InfoBox type='warning'>
|
||||||
This section is under development as part of the project CLPV Tool. For more details and progress <a href="https://github.com/colouring-cities/manual/wiki/G2.-Data-capture-(2).-Live-streaming-and-automated-methods">read here</a>.
|
This section is under development as part of the project CLPV Tool. For more details and progress <a href="https://github.com/colouring-cities/manual/wiki/G2.-Data-capture-(2).-Live-streaming-and-automated-methods">read here</a>.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
<PlanningDataOfficialDataEntry
|
||||||
|
value={props.building.planning_data}
|
||||||
|
/>
|
||||||
<DataEntry
|
<DataEntry
|
||||||
title={dataFields.planning_portal_link.title}
|
title={dataFields.planning_portal_link.title}
|
||||||
slug="planning_portal_link"
|
slug="planning_portal_link"
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
interface InfoBoxProps {
|
interface InfoBoxProps {
|
||||||
msg?: string;
|
msg?: string;
|
||||||
type?: 'info' | 'warning'
|
type?: 'info' | 'warning' | 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoBox: React.FC<InfoBoxProps> = ({msg, children, type = 'info'}) => (
|
const InfoBox: React.FC<InfoBoxProps> = ({msg, children, type = 'info'}) => (
|
||||||
|
@ -167,7 +167,21 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[Category.Planning]: [{
|
[Category.Planning]: [
|
||||||
|
{
|
||||||
|
mapStyle: 'planning_applications_status',
|
||||||
|
legend: {
|
||||||
|
title: 'Planning applications',
|
||||||
|
elements: [
|
||||||
|
{ color: '#00ffff', text: 'Submitted' },
|
||||||
|
{ color: '#00ff00', text: 'Approved' },
|
||||||
|
{ color: '#ffff00', text: 'Appeal In Progress' },
|
||||||
|
{ color: '#ff0000', text: 'Refused' },
|
||||||
|
{ color: '#999999', text: 'Withdrawn' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
mapStyle: 'planning_combined',
|
mapStyle: 'planning_combined',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Designation/protection',
|
title: 'Designation/protection',
|
||||||
|
@ -170,6 +170,14 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
example: [{uprn: "", parent_uprn: "" }, {uprn: "", parent_uprn: "" }],
|
example: [{uprn: "", parent_uprn: "" }, {uprn: "", parent_uprn: "" }],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
planning_data: {
|
||||||
|
category: Category.Location,
|
||||||
|
title: "PLANNING DATA",
|
||||||
|
tooltip: "PLANNING DATA",
|
||||||
|
example: [{}],
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
ref_osm_id: {
|
ref_osm_id: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "OSM ID",
|
title: "OSM ID",
|
||||||
|
@ -11,6 +11,7 @@ export type BuildingMapTileset = 'date_year' |
|
|||||||
'community_local_significance_total' |
|
'community_local_significance_total' |
|
||||||
'community_expected_planning_application_total' |
|
'community_expected_planning_application_total' |
|
||||||
'community_in_public_ownership' |
|
'community_in_public_ownership' |
|
||||||
|
'planning_applications_status' |
|
||||||
'planning_combined' |
|
'planning_combined' |
|
||||||
'sust_dec' |
|
'sust_dec' |
|
||||||
'building_attachment_form' |
|
'building_attachment_form' |
|
||||||
|
@ -7,6 +7,7 @@ import serialize from 'serialize-javascript';
|
|||||||
import {
|
import {
|
||||||
getBuildingById,
|
getBuildingById,
|
||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
|
getBuildingPlanningDataById,
|
||||||
getLatestRevisionId,
|
getLatestRevisionId,
|
||||||
getUserVerifiedAttributes
|
getUserVerifiedAttributes
|
||||||
} from './api/services/building/base';
|
} from './api/services/building/base';
|
||||||
@ -33,13 +34,14 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let [user, building, uprns, userVerified, latestRevisionId] = await Promise.all([
|
let [user, building, uprns, planningData, userVerified, latestRevisionId] = await Promise.all([
|
||||||
userId ? getUserById(userId) : undefined,
|
userId ? getUserById(userId) : undefined,
|
||||||
isBuilding ? getBuildingById(
|
isBuilding ? getBuildingById(
|
||||||
buildingId,
|
buildingId,
|
||||||
{ userDataOptions: userId ? { userId, userAttributes: true } : null }
|
{ userDataOptions: userId ? { userId, userAttributes: true } : null }
|
||||||
) : undefined,
|
) : undefined,
|
||||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||||
|
isBuilding ? getBuildingPlanningDataById(buildingId) : undefined,
|
||||||
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||||
getLatestRevisionId()
|
getLatestRevisionId()
|
||||||
]);
|
]);
|
||||||
@ -53,6 +55,9 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
|||||||
if (data.building != null) {
|
if (data.building != null) {
|
||||||
data.building.uprns = uprns;
|
data.building.uprns = uprns;
|
||||||
}
|
}
|
||||||
|
if (data.building != null) {
|
||||||
|
data.building.planning_data = planningData;
|
||||||
|
}
|
||||||
data.latestRevisionId = latestRevisionId;
|
data.latestRevisionId = latestRevisionId;
|
||||||
renderHTML(context, data, req, res);
|
renderHTML(context, data, req, res);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
@ -136,6 +136,11 @@ const LAYER_QUERIES = {
|
|||||||
WHERE
|
WHERE
|
||||||
community_public_ownership IS NOT NULL
|
community_public_ownership IS NOT NULL
|
||||||
`,
|
`,
|
||||||
|
planning_applications_status: `SELECT
|
||||||
|
buildings.geometry_id, building_properties.uprn, building_properties.building_id, planning_data.status AS status, planning_data.uprn
|
||||||
|
FROM building_properties
|
||||||
|
INNER JOIN planning_data ON building_properties.uprn = planning_data.uprn
|
||||||
|
INNER JOIN buildings ON building_properties.building_id = buildings.building_id`,
|
||||||
planning_combined: `
|
planning_combined: `
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id,
|
geometry_id,
|
||||||
|
2
etl/planning_data/.gitignore
vendored
Normal file
2
etl/planning_data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.json
|
||||||
|
*.sql
|
33
etl/planning_data/README.MD
Normal file
33
etl/planning_data/README.MD
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
Following scripts should be scheduled to run regularly to load livestream data into database.
|
||||||
|
|
||||||
|
```
|
||||||
|
# querying API to obtain data
|
||||||
|
python3 obtain_livestream_data.py > all_data.json
|
||||||
|
|
||||||
|
# loading data into Colouring database
|
||||||
|
python3 load_into_database
|
||||||
|
|
||||||
|
# removing tile cache for planning_applications_status layer - note that location of cache depends on your configuration
|
||||||
|
rm /srv/colouring-london/tilecache/planning_applications_status/* -rf
|
||||||
|
```
|
||||||
|
|
||||||
|
As loading into databases expects environment variables to be set, one option to actually schedult it in a cron is something like
|
||||||
|
|
||||||
|
```
|
||||||
|
export $(cat ~/scripts/.env | xargs) && /usr/bin/python3 ~/colouring-london/etl/planning_data/load_into_database.py
|
||||||
|
```
|
||||||
|
|
||||||
|
with
|
||||||
|
|
||||||
|
```
|
||||||
|
~/script/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
being in following format
|
||||||
|
|
||||||
|
```
|
||||||
|
PGHOST=localhost
|
||||||
|
PGDATABASE=colouringlondondb
|
||||||
|
PGUSER=cldbadmin
|
||||||
|
PGPASSWORD=actualpassword
|
||||||
|
```
|
109
etl/planning_data/load_into_database.py
Normal file
109
etl/planning_data/load_into_database.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import psycopg2
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=os.environ['PGHOST'],
|
||||||
|
dbname=os.environ['PGDATABASE'],
|
||||||
|
user=os.environ['PGUSER'],
|
||||||
|
password=os.environ['PGPASSWORD']
|
||||||
|
)
|
||||||
|
|
||||||
|
def filepath():
|
||||||
|
return os.path.dirname(os.path.realpath(__file__)) + os.sep + "data.json"
|
||||||
|
|
||||||
|
def insert_entry(connection, e):
|
||||||
|
elements = []
|
||||||
|
application_url = "NULL"
|
||||||
|
if e["application_url"] != None:
|
||||||
|
application_url = "'" + e["application_url"] + "'"
|
||||||
|
with connection.cursor() as cur:
|
||||||
|
cur.execute('''INSERT INTO
|
||||||
|
planning_data (planning_application_id, planning_application_link, description, decision_date, last_synced_date, status, data_source, data_source_link, uprn)
|
||||||
|
VALUES
|
||||||
|
(%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
''', (e["application_id"], application_url, e["description"], e["decision_date"], e["last_synced_date"], e["status"], e["data_source"], e["data_source_link"], e["uprn"]))
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return """INSERT INTO planning_data
|
||||||
|
(planning_application_id, planning_application_link, description, decision_date, last_synced_date, status, data_source, data_source_link, uprn)
|
||||||
|
VALUES""" + ",\n".join(elements) + ";"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date_string_into_datestring(incoming):
|
||||||
|
date = None
|
||||||
|
try:
|
||||||
|
date = datetime.datetime.strptime(incoming, "%d/%m/%Y") # '21/07/2022'
|
||||||
|
except ValueError:
|
||||||
|
date = datetime.datetime.strptime(incoming, "%Y-%m-%dT%H:%M:%S.%fZ") # '2022-08-08T20:07:22.238Z'
|
||||||
|
return datetime.datetime.strftime(date, "%Y-%m-%d")
|
||||||
|
|
||||||
|
def shorten_description(original_description):
|
||||||
|
description = original_description.strip()
|
||||||
|
limit = 400
|
||||||
|
if len(description) > limit:
|
||||||
|
description = ""
|
||||||
|
for entry in original_description.split():
|
||||||
|
extended = description
|
||||||
|
if extended != "":
|
||||||
|
extended += " "
|
||||||
|
extended += entry
|
||||||
|
if len(extended) <= limit:
|
||||||
|
description = extended
|
||||||
|
if description == "":
|
||||||
|
description = description[0:limit]
|
||||||
|
description += "... <i>(show more)</i>"
|
||||||
|
return description
|
||||||
|
|
||||||
|
def main():
|
||||||
|
connection = get_connection()
|
||||||
|
with connection.cursor() as cur:
|
||||||
|
cur.execute("TRUNCATE planning_data")
|
||||||
|
with open(filepath(), 'r') as content_file:
|
||||||
|
data = json.load(content_file)
|
||||||
|
if data['rawResponse']['timed_out']:
|
||||||
|
raise Exception("query getting livestream data has failed")
|
||||||
|
if data['is_partial']:
|
||||||
|
raise Exception("query getting livestream data has failed")
|
||||||
|
if data['is_running']:
|
||||||
|
raise Exception("query getting livestream data has failed")
|
||||||
|
for entry in data['rawResponse']['hits']['hits']:
|
||||||
|
description = shorten_description(entry['_source']['description'])
|
||||||
|
application_id = entry['_source']['id']
|
||||||
|
decision_date = parse_date_string_into_datestring(entry['_source']['decision_date'])
|
||||||
|
last_synced_date = parse_date_string_into_datestring(entry['_source']['last_synced'])
|
||||||
|
uprn = entry['_source']['uprn']
|
||||||
|
status = entry['_source']['status']
|
||||||
|
if status in ["No Objection to Proposal (OBS only)", "Not Required", None, "Lapsed", "Unknown", "SECS", "Comment Issued"]:
|
||||||
|
continue
|
||||||
|
if status in []:
|
||||||
|
opts = jsbeautifier.default_options()
|
||||||
|
opts.indent_size = 2
|
||||||
|
print(jsbeautifier.beautify(json.dumps(entry), opts))
|
||||||
|
continue
|
||||||
|
if status == "Refused":
|
||||||
|
status = "Rejected"
|
||||||
|
if status == "Appeal Received":
|
||||||
|
status = "Appeal In Progress"
|
||||||
|
if (status not in ["Approved", "Rejected", "Appeal In Progress", "Withdrawn", ]):
|
||||||
|
raise Exception("Unexpected status " + status)
|
||||||
|
description = entry['_source']['description'].strip()
|
||||||
|
if uprn == None:
|
||||||
|
continue
|
||||||
|
entry = {
|
||||||
|
"description": description,
|
||||||
|
"decision_date": decision_date,
|
||||||
|
"last_synced_date": last_synced_date,
|
||||||
|
"application_id": application_id,
|
||||||
|
"application_url": entry['_source']['url_planning_app'],
|
||||||
|
"uprn": uprn,
|
||||||
|
"status": status,
|
||||||
|
"data_source": "The Planning London DataHub Greater London Authority",
|
||||||
|
"data_source_link": "https://data.london.gov.uk/dataset/planning-london-datahub?_gl=1%2aprwpc%2a_ga%2aMzQyOTg0MjcxLjE2NTk0NDA4NTM", # TODO test
|
||||||
|
}
|
||||||
|
insert_entry(connection, entry)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
55
etl/planning_data/make_query.py
Normal file
55
etl/planning_data/make_query.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
def obtain_data(data):
|
||||||
|
url = "https://planningdata.london.gov.uk/dashboard/internal/search/es"
|
||||||
|
headers = headers_of_query()
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||||
|
# typically initially return something like that
|
||||||
|
# {'id': 'Fmo0RW9DX0k5U3UtLWJIVlEtMzRwR3cfdGwtYkJaaHNUeG1GdF9kRHFtQldaUToxODczMzM5Nw==', 'is_partial': True, 'is_running': True, 'rawResponse': {'took': 100, 'timed_out': False, 'terminated_early': False, 'num_reduce_phases': 0, '_shards': {'total': 1, 'successful': 0, 'skipped': 0, 'failed': 0}, 'hits': {'total': 0, 'max_score': None, 'hits': []}}, 'total': 1, 'loaded': 0}
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception("unexpected status code " + str(response.status_code))
|
||||||
|
|
||||||
|
output = response.content.decode('utf-8')
|
||||||
|
|
||||||
|
output = json.loads(output)
|
||||||
|
if output["is_partial"]:
|
||||||
|
identifier = output["id"]
|
||||||
|
while output["is_partial"]:
|
||||||
|
time.sleep(3)
|
||||||
|
response = reask_for_query_results(identifier)
|
||||||
|
output = json.loads(response.content.decode('utf-8'))
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception("unexpected status code " +
|
||||||
|
str(response.status_code))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def headers_of_query():
|
||||||
|
headers = CaseInsensitiveDict()
|
||||||
|
headers["User-Agent"] = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0"
|
||||||
|
headers["Accept"] = "*/*"
|
||||||
|
headers["Referer"] = "https://planningdata.london.gov.uk/dashboard/app/discover"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
headers["kbn-version"] = "7.9.3"
|
||||||
|
headers["Origin"] = "https://planningdata.london.gov.uk"
|
||||||
|
headers["Connection"] = "keep-alive"
|
||||||
|
headers["Sec-Fetch-Dest"] = "empty"
|
||||||
|
headers["Sec-Fetch-Mode"] = "cors"
|
||||||
|
headers["Sec-Fetch-Site"] = "same-origin"
|
||||||
|
headers["TE"] = "trailers"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def reask_for_query_results(identifier):
|
||||||
|
data = {'id': identifier}
|
||||||
|
return requests.post(
|
||||||
|
'https://planningdata.london.gov.uk/dashboard/internal/search/es',
|
||||||
|
data=data,
|
||||||
|
timeout=100000,
|
||||||
|
headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0',
|
||||||
|
'kbn-version': '7.9.3',
|
||||||
|
}
|
||||||
|
)
|
87
etl/planning_data/obtain_livestream_data.py
Normal file
87
etl/planning_data/obtain_livestream_data.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
import jsbeautifier
|
||||||
|
|
||||||
|
import make_query
|
||||||
|
|
||||||
|
def main():
|
||||||
|
output = make_query.obtain_data(get_query())
|
||||||
|
# print(json.dumps(output))
|
||||||
|
opts = jsbeautifier.default_options()
|
||||||
|
opts.indent_size = 2
|
||||||
|
print(jsbeautifier.beautify(json.dumps(output), opts))
|
||||||
|
|
||||||
|
|
||||||
|
def get_query():
|
||||||
|
true = True # makes possible to copy JSON into Python code
|
||||||
|
return {
|
||||||
|
"params": {
|
||||||
|
"ignoreThrottled": true,
|
||||||
|
"index": "applications",
|
||||||
|
"body": {
|
||||||
|
"version": true,
|
||||||
|
"size": 500,
|
||||||
|
"sort": [
|
||||||
|
{
|
||||||
|
"last_updated": {
|
||||||
|
"order": "desc",
|
||||||
|
"unmapped_type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aggs": {
|
||||||
|
"2": {
|
||||||
|
"date_histogram": {
|
||||||
|
"field": "last_updated",
|
||||||
|
"calendar_interval": "1d",
|
||||||
|
"time_zone": "Europe/Warsaw",
|
||||||
|
"min_doc_count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stored_fields": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"script_fields": {},
|
||||||
|
"docvalue_fields": [],
|
||||||
|
"_source": {
|
||||||
|
"excludes": []
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"bool": {
|
||||||
|
"must": [],
|
||||||
|
"filter": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"decision_date": {
|
||||||
|
"gte": "1922-01-01T00:00:00.000Z",
|
||||||
|
"format": "strict_date_optional_time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"should": [],
|
||||||
|
"must_not": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"highlight": {
|
||||||
|
"pre_tags": [
|
||||||
|
"@kibana-highlighted-field@"
|
||||||
|
],
|
||||||
|
"post_tags": [
|
||||||
|
"@/kibana-highlighted-field@"
|
||||||
|
],
|
||||||
|
"fields": {
|
||||||
|
"*": {}
|
||||||
|
},
|
||||||
|
"fragment_size": 2147483647
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rest_total_hits_as_int": true,
|
||||||
|
"ignore_unavailable": true,
|
||||||
|
"ignore_throttled": true,
|
||||||
|
"timeout": "30000ms"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
1
migrations/033.planning_livestream_data.down.sql
Normal file
1
migrations/033.planning_livestream_data.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS planning_data;
|
15
migrations/033.planning_livestream_data.up.sql
Normal file
15
migrations/033.planning_livestream_data.up.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS planning_data (
|
||||||
|
-- internal unique id
|
||||||
|
planning_entry_id serial PRIMARY KEY,
|
||||||
|
|
||||||
|
-- assigned by planning authority
|
||||||
|
planning_application_id VARCHAR(50),
|
||||||
|
planning_application_link VARCHAR(260),
|
||||||
|
description VARCHAR,
|
||||||
|
decision_date date,
|
||||||
|
last_synced_date date,
|
||||||
|
status VARCHAR(20),
|
||||||
|
data_source VARCHAR(70),
|
||||||
|
data_source_link VARCHAR(150),
|
||||||
|
uprn bigint
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user