Merge branch 'feature/bulk_data_sources' into features/migrations_landuse

This commit is contained in:
Maciej Ziarkowski 2019-12-02 18:13:55 +00:00
commit bfdde02fab
68 changed files with 6670 additions and 3850 deletions

7347
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,23 +12,24 @@
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/fontawesome-svg-core": "^1.2.21",
"@fortawesome/free-solid-svg-icons": "^5.10.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@mapbox/sphericalmercator": "^1.1.0",
"body-parser": "^1.19.0",
"bootstrap": "^4.3.1",
"connect-pg-simple": "^5.0.0",
"connect-pg-simple": "^6.0.1",
"express": "^4.17.1",
"express-session": "^1.16.2",
"leaflet": "^1.5.1",
"mapnik": "^4.2.1",
"node-fs": "^0.1.7",
"pg-promise": "^8.7.3",
"nodemailer": "^6.3.0",
"pg-promise": "^8.7.5",
"prop-types": "^15.7.2",
"query-string": "^6.8.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"query-string": "^6.8.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-leaflet": "^1.0.1",
"react-leaflet-universal": "^1.2.0",
"react-router-dom": "^4.3.1",
@ -36,10 +37,45 @@
"sharp": "^0.21.3"
},
"devDependencies": {
"@types/express": "^4.17.0",
"@types/express-session": "^1.15.13",
"@types/jest": "^24.0.17",
"@types/node": "^8.10.52",
"@types/nodemailer": "^6.2.1",
"@types/prop-types": "^15.7.1",
"@types/react": "^16.9.1",
"@types/react-dom": "^16.8.5",
"@types/react-router-dom": "^4.3.4",
"@types/webpack-env": "^1.14.0",
"babel-eslint": "^10.0.2",
"eslint": "^5.16.0",
"eslint-plugin-jest": "^22.7.2",
"eslint-plugin-react": "^7.14.2",
"razzle": "^3.0.0"
"eslint-plugin-jest": "^22.15.0",
"eslint-plugin-react": "^7.14.3",
"razzle": "^3.0.0",
"razzle-plugin-typescript": "^3.0.0",
"ts-jest": "^24.0.2",
"tslint": "^5.18.0",
"tslint-react": "^4.0.0",
"typescript": "^3.5.3"
},
"jest": {
"transform": {
"\\.(ts|tsx)$": "ts-jest",
"\\.css$": "<rootDir>/node_modules/razzle/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|css|json)$)": "<rootDir>/node_modules/razzle/config/jest/fileTransform.js"
},
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.(ts|js)?(x)",
"<rootDir>/src/**/?(*.)(spec|test).(ts|js)?(x)"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
]
}
}

View File

@ -1,4 +1,5 @@
module.exports = {
plugins: ['typescript'],
modify: (config, { target, dev }, webpack) => {
// load webfonts
rules = config.module.rules || [];

137
app/src/api/api.ts Normal file
View File

@ -0,0 +1,137 @@
import express from 'express';
import bodyParser from 'body-parser';
import { authUser, createUser, getUserById, getNewUserAPIKey, deleteUser, logout } from './services/user';
import { queryLocation } from './services/search';
import buildingsRouter from './routes/buildingsRouter';
import usersRouter from './routes/usersRouter';
const server = express.Router();
// parse POSTed json body
server.use(bodyParser.json());
server.use('/buildings', buildingsRouter);
server.use('/users', usersRouter);
// GET own user info
server.route('/users/me')
.get(function (req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
getUserById(req.session.user_id).then(function (user) {
res.send(user);
}).catch(function (error) {
res.send(error);
});
})
.delete((req, res) => {
if (!req.session.user_id) {
return res.send({ error: 'Must be logged in' });
}
console.log(`Deleting user ${req.session.user_id}`);
deleteUser(req.session.user_id).then(
() => logout(req.session)
).then(() => {
res.send({ success: true });
}).catch(err => {
res.send({ error: err });
});
})
// POST user auth
server.post('/login', function (req, res) {
authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any
if (user.user_id) {
req.session.user_id = user.user_id;
} else {
req.session.user_id = undefined;
}
res.send(user);
}).catch(function (error) {
res.send(error);
})
});
// POST user logout
server.post('/logout', function (req, res) {
logout(req.session).then(() => {
res.send({ success: true });
}).catch(err => {
console.error(err);
res.send({ error: 'Failed to end session'});
});
});
// POST generate API key
server.post('/api/key', function (req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
getNewUserAPIKey(req.session.user_id).then(function (apiKey) {
res.send(apiKey);
}).catch(function (error) {
res.send(error);
});
})
// GET search
server.get('/search', function (req, res) {
const searchTerm = req.query.q;
if (!searchTerm) {
res.send({
error: 'Please provide a search term'
})
return
}
queryLocation(searchTerm).then((results) => {
if (typeof (results) === 'undefined') {
res.send({
error: 'Database error'
})
return
}
res.send({
results: results.map(item => {
// map from DB results to GeoJSON Feature objects
const geom = JSON.parse(item.st_asgeojson)
return {
type: 'Feature',
attributes: {
label: item.search_str,
zoom: item.zoom || 9
},
geometry: geom
}
})
})
}).catch(function (error) {
res.send(error);
});
})
server.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
if (err != undefined) {
console.log('Global error handler: ', err);
res.status(500).send({ error: 'Server error' });
}
});
server.use((req, res) => {
res.status(404).json({ error: 'Resource not found'});
})
export default server;

View File

@ -0,0 +1,154 @@
import * as buildingService from '../services/building';
import * as userService from '../services/user';
// GET buildings
// not implemented - may be useful to GET all buildings, paginated
// GET buildings at point
function getBuildingsByLocation(req, res) {
const { lng, lat } = req.query;
buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
}
// GET buildings by reference (UPRN/TOID or other identifier)
function getBuildingsByReference(req, res) {
const { key, id } = req.query;
buildingService.queryBuildingsByReference(key, id).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
}
// GET individual building, POST building updates
function getBuildingById(req, res) {
const { building_id } = req.params;
buildingService.getBuildingById(building_id).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
}
function updateBuildingById(req, res) {
if (req.session.user_id) {
updateBuilding(req, res, req.session.user_id);
} else if (req.query.api_key) {
userService.authAPIUser(req.query.api_key)
.then(function (user) {
updateBuilding(req, res, user.user_id)
})
.catch(function (err) {
console.error(err);
res.send({ error: 'Must be logged in' });
});
} else {
res.send({ error: 'Must be logged in' });
}
}
function updateBuilding(req, res, userId) {
const { building_id } = req.params;
const building = req.body;
buildingService.saveBuilding(building_id, building, userId).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
}
// GET building UPRNs
function getBuildingUPRNsById(req, res) {
const { building_id } = req.params;
buildingService.getBuildingUPRNsById(building_id).then(function (result) {
if (typeof (result) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send({
uprns: result
});
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
}
// GET/POST like building
function getBuildingLikeById(req, res) {
if (!req.session.user_id) {
res.send({ like: false }); // not logged in, so cannot have liked
return
}
const { building_id } = req.params;
buildingService.getBuildingLikeById(building_id, req.session.user_id).then(like => {
// any value returned means like
res.send({ like: like })
}).catch(
() => res.send({ error: 'Database error' })
)
}
function updateBuildingLikeById(req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
const { building_id } = req.params;
const { like } = req.body;
if (like) {
buildingService.likeBuilding(building_id, req.session.user_id).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
} else {
buildingService.unlikeBuilding(building_id, req.session.user_id).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
}
}
export default {
getBuildingsByLocation,
getBuildingsByReference,
getBuildingById,
updateBuildingById,
getBuildingUPRNsById,
getBuildingLikeById,
updateBuildingLikeById
};

View File

@ -0,0 +1,111 @@
import { URL } from 'url';
import express from 'express';
import * as userService from '../services/user';
import * as passwordResetService from '../services/passwordReset';
import { TokenVerificationError } from '../services/passwordReset';
function createUser(req, res) {
const user = req.body;
if (req.session.user_id) {
res.send({ error: 'Already signed in' });
return;
}
if (user.email) {
if (user.email != user.confirm_email) {
res.send({ error: 'Email did not match confirmation.' });
return;
}
} else {
user.email = null;
}
userService.createUser(user).then(function (result) {
if (result.user_id) {
req.session.user_id = result.user_id;
res.send({ user_id: result.user_id });
} else {
req.session.user_id = undefined;
res.send({ error: result.error });
}
}).catch(function (err) {
console.error(err);
res.send(err);
});
}
function getCurrentUser(req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return;
}
userService.getUserById(req.session.user_id).then(function (user) {
res.send(user);
}).catch(function (error) {
res.send(error);
});
}
function deleteCurrentUser(req, res) {
if (!req.session.user_id) {
return res.send({ error: 'Must be logged in' });
}
console.log(`Deleting user ${req.session.user_id}`);
userService.deleteUser(req.session.user_id).then(
() => userService.logout(req.session)
).then(() => {
res.send({ success: true });
}).catch(err => {
res.send({ error: err });
});
}
async function resetPassword(req: express.Request, res: express.Response) {
if(req.body == undefined || (req.body.email == undefined && req.body.token == undefined)) {
return res.send({ error: 'Expected an email address or password reset token in the request body' });
}
if(req.body.email != undefined) {
// first stage: send reset token to email address
const origin = getWebAppOrigin();
await passwordResetService.sendPasswordResetToken(req.body.email, origin);
return res.status(202).send({ success: true });
} else if (req.body.token != undefined) {
// second stage: verify token and reset password
if (req.body.password == undefined) {
return res.send({ error: 'Expected a new password' });
}
try {
await passwordResetService.resetPassword(req.body.token, req.body.password);
} catch (err) {
if (err instanceof TokenVerificationError) {
return res.send({ error: 'Could not verify token' });
}
throw err;
}
return res.send({ success: true });
}
}
function getWebAppOrigin() : string {
const origin = process.env.WEBAPP_ORIGIN;
if (origin == undefined) {
throw new Error('WEBAPP_ORIGIN not defined');
}
return origin;
}
export default {
createUser,
getCurrentUser,
deleteCurrentUser,
resetPassword
};

View File

@ -0,0 +1,32 @@
import express from 'express';
import buildingController from '../controllers/buildingController';
const router = express.Router();
// GET buildings
// not implemented - may be useful to GET all buildings, paginated
// GET buildings at point
router.get('/locate', buildingController.getBuildingsByLocation);
// GET buildings by reference (UPRN/TOID or other identifier)
router.get('/reference', buildingController.getBuildingsByReference);
router.route('/:building_id.json')
// GET individual building
.get(buildingController.getBuildingById)
// POST building updates
.post(buildingController.updateBuildingById);
// GET building UPRNs
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
// GET/POST like building
router.route('/:building_id/like.json')
.get(buildingController.getBuildingLikeById)
.post(buildingController.updateBuildingLikeById);
export default router;

View File

@ -0,0 +1,21 @@
import express from 'express';
import userController from '../controllers/userController';
const asyncMiddleware = fn =>
(req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next);
};
const router = express.Router();
router.post('/', userController.createUser);
router.route('/me')
.get(userController.getCurrentUser)
.delete(userController.deleteCurrentUser);
router.put('/password', asyncMiddleware(userController.resetPassword));
export default router;

View File

@ -2,8 +2,8 @@
* Building data access
*
*/
import db from '../db';
import { removeAllAtBbox } from '../tiles/cache';
import db from '../../db';
import { removeAllAtBbox } from '../../tiles/cache';
// data type note: PostgreSQL bigint (64-bit) is handled as string in JavaScript, because of
// JavaScript numerics are 64-bit double, giving only partial coverage.
@ -72,7 +72,7 @@ function queryBuildingsByReference(key, id) {
return undefined;
});
}
return { error: 'Key must be UPRN or TOID' };
return Promise.resolve({ error: 'Key must be UPRN or TOID' });
}
function getBuildingById(id) {

View File

@ -0,0 +1,11 @@
import * as nodemailer from 'nodemailer';
export const transporter = nodemailer.createTransport({
host: process.env.MAIL_SERVER_HOST,
port: parseInt(process.env.MAIL_SERVER_PORT),
secure: false,
auth: {
user: process.env.MAIL_SERVER_USER,
pass: process.env.MAIL_SERVER_PASSWORD
}
});

View File

@ -0,0 +1,121 @@
import url, { URL } from 'url';
import { errors } from 'pg-promise';
import nodemailer from 'nodemailer';
import db from '../../db';
import * as userService from './user';
import { transporter } from './email';
/**
* Generate a password reset token for the specified account and send the password reset link by email
* @param email the email address for which to generate a password reset token
* @param siteOrigin the origin of the website, without a path element - e.g. https://beta.colouring.london
*/
export async function sendPasswordResetToken(email: string, siteOrigin: string): Promise<void> {
const user = await userService.getUserByEmail(email);
// if no user found for email, do nothing
if (user == undefined) return;
const token = await createPasswordResetToken(user.user_id);
const message = getPasswordResetEmail(email, token, siteOrigin);
await transporter.sendMail(message);
}
export async function resetPassword(passwordResetToken: string, newPassword: string): Promise<void> {
const userId = await verifyPasswordResetToken(passwordResetToken);
if (userId != undefined) {
await updatePasswordForUser(userId, newPassword);
//TODO: expire other tokens of the user?
} else {
throw new TokenVerificationError('Password reset token could not be verified');
}
}
export class TokenVerificationError extends Error {
constructor(message: string) {
super(message);
this.name = "TokenVerificationError";
}
}
function getPasswordResetEmail(email: string, token: string, siteOrigin: string): nodemailer.SendMailOptions {
const linkUrl = new URL(siteOrigin);
linkUrl.pathname = '/password-reset.html';
linkUrl.search = `?token=${token}`;
const linkString = url.format(linkUrl);
const messageBody = `Hi there,
Someone has requested a password reset for the Colouring London account associated with this email address.
Click on the following link within the next 24 hours to reset your password:
${linkString}
`;
return {
text: messageBody,
subject: 'Reset your Colouring London password',
to: email,
from: 'no-reply@colouring.london'
};
}
async function createPasswordResetToken(userId: string) {
const { token } = await db.one(
`INSERT INTO
user_password_reset_tokens (user_id, expires_on)
VALUES
($1::uuid, now() at time zone 'utc' + INTERVAL '$2 day')
RETURNING
token
`, [userId, 1]
);
return token;
}
/**
* Verify that the password reset token is valid and expire the token
* @param token password reset token to verify and use
* @returns the UUID of the user whose token was used
*/
async function verifyPasswordResetToken(token: string): Promise<string> {
try {
// verify and deactivate the token in one operation
const usedToken = await db.one(
`UPDATE
user_password_reset_tokens
SET used = true
WHERE
token = $1::uuid
AND NOT used
AND now() at time zone 'utc' < expires_on
RETURNING *`, [token]
);
console.log('verified');
return usedToken.user_id;
} catch (err) {
if (err instanceof errors.QueryResultError) {
console.log(err.code);
if (err.code === errors.queryResultErrorCode.noData) return undefined;
}
throw err;
}
}
async function updatePasswordForUser(userId: string, newPassword: string): Promise<null> {
return db.none(
`UPDATE
users
SET
pass = crypt($1, gen_salt('bf'))
WHERE
user_id = $2::uuid
`, [newPassword, userId]);
}

View File

@ -6,7 +6,7 @@
* - this DOES expose geometry, another reason to keep this clearly separated from building
* data
*/
import db from '../db';
import db from '../../db';
function queryLocation(term) {
const limit = 5;

View File

@ -2,7 +2,7 @@
* User data access
*
*/
import db from '../db';
import db from '../../db';
function createUser(user) {
if (!user.password || user.password.length < 8) {
@ -86,6 +86,21 @@ function getUserById(id) {
});
}
function getUserByEmail(email: string) {
return db.one(
`SELECT
user_id, username, email
FROM
users
WHERE
email = $1
`, [email]
).catch(function(error) {
console.error('Error:', error);
return undefined;
});
}
function getNewUserAPIKey(id) {
return db.one(
`UPDATE
@ -122,4 +137,41 @@ function authAPIUser(key) {
});
}
export { getUserById, createUser, authUser, getNewUserAPIKey, authAPIUser }
function deleteUser(id) {
return db.none(
`UPDATE users
SET
email = null,
pass = null,
api_key = null,
username = concat('deleted_', cast(user_id as char(13))),
is_deleted = true,
deleted_on = now() at time zone 'utc'
WHERE user_id = $1
`, [id]
).catch((error) => {
console.error('Error:', error);
return {error: 'Database error'};
});
}
function logout(session: Express.Session) {
return new Promise((resolve, reject) => {
session.user_id = undefined;
session.destroy(err => {
if (err) return reject(err);
return resolve();
});
});
}
export {
getUserById,
getUserByEmail,
createUser,
authUser,
getNewUserAPIKey,
authAPIUser,
deleteUser,
logout
};

View File

@ -8,7 +8,7 @@ import { hydrate } from 'react-dom';
import App from './frontend/app';
const data = window.__PRELOADED_STATE__;
const data = (window as any).__PRELOADED_STATE__; // TODO: remove any
hydrate(
<BrowserRouter>

View File

@ -15,10 +15,10 @@ const pgp = pg();
// database connection (default to env vars)
const db = pgp({
'host': process.env.PGHOST,
'dbname': process.env.PGDATABASE,
'database': process.env.PGDATABASE,
'user': process.env.PGUSER,
'password': process.env.PGPASSWORD,
'port': process.env.PGPORT
'port': parseInt(process.env.PGPORT)
});
export default db;

View File

@ -18,6 +18,10 @@ import MyAccountPage from './my-account';
import SignUp from './signup';
import Welcome from './welcome';
import { parseCategoryURL } from '../parse';
import PrivacyPolicyPage from './privacy-policy';
import ContributorAgreementPage from './contributor-agreement';
import ForgottenPassword from './forgotten-password';
import PasswordReset from './password-reset';
/**
* App component
@ -31,7 +35,13 @@ import { parseCategoryURL } from '../parse';
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
* child components to navigate without a full page reload.
*/
class App extends React.Component {
class App extends React.Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
user: PropTypes.object,
building: PropTypes.object,
building_like: PropTypes.bool
}
constructor(props) {
super(props);
// set building revision id, default 0
@ -77,7 +87,7 @@ class App extends React.Component {
selectBuilding(building) {
this.increaseRevision(building.revision_id);
// get UPRNs and update
fetch(`/building/${building.building_id}/uprns.json`, {
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
method: 'GET',
headers:{
'Content-Type': 'application/json'
@ -98,7 +108,7 @@ class App extends React.Component {
});
// get if liked and update
fetch(`/building/${building.building_id}/like.json`, {
fetch(`/api/buildings/${building.building_id}/like.json`, {
method: 'GET',
headers:{
'Content-Type': 'application/json'
@ -130,7 +140,7 @@ class App extends React.Component {
colourBuilding(building) {
const cat = parseCategoryURL(window.location.pathname);
const q = parse(window.location.search);
const data = (cat === 'like')? {like: true}: JSON.parse(q.data);
const data = (cat === 'like')? {like: true}: JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
if (cat === 'like'){
this.likeBuilding(building.building_id)
} else {
@ -139,7 +149,7 @@ class App extends React.Component {
}
likeBuilding(buildingId) {
fetch(`/building/${buildingId}/like.json`, {
fetch(`/api/buildings/${buildingId}/like.json`, {
method: 'POST',
headers:{
'Content-Type': 'application/json'
@ -160,7 +170,7 @@ class App extends React.Component {
}
updateBuilding(buildingId, data){
fetch(`/building/${buildingId}.json`, {
fetch(`/api/buildings/${buildingId}.json`, {
method: 'POST',
body: JSON.stringify(data),
headers:{
@ -239,6 +249,8 @@ class App extends React.Component {
<Route exact path="/login.html">
<Login user={this.state.user} login={this.login} />
</Route>
<Route exact path="/forgotten-password.html" component={ForgottenPassword} />
<Route exact path="/password-reset.html" component={PasswordReset} />
<Route exact path="/sign-up.html">
<SignUp user={this.state.user} login={this.login} />
</Route>
@ -249,6 +261,8 @@ class App extends React.Component {
logout={this.logout}
/>
</Route>
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
<Route component={NotFound} />
</Switch>
</main>
@ -257,12 +271,6 @@ class App extends React.Component {
}
}
App.propTypes = {
user: PropTypes.object,
building: PropTypes.object,
building_like: PropTypes.bool
}
/**
* Component to fall back on in case of 404 or no other match
*/

View File

@ -47,17 +47,36 @@ BuildingEdit.propTypes = {
building_id: PropTypes.number
}
class EditForm extends Component {
class EditForm extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
title: PropTypes.string,
slug: PropTypes.string,
cat: PropTypes.string,
help: PropTypes.string,
error: PropTypes.object,
like: PropTypes.bool,
building_like: PropTypes.bool,
selectBuilding: PropTypes.func,
building_id: PropTypes.number,
inactive: PropTypes.bool,
fields: PropTypes.array
};
constructor(props) {
super(props);
// create object and spread into state to avoid TS complaining about modifying readonly state
let fieldsObj = {};
for (const field of props.fields) {
fieldsObj[field.slug] = props[field.slug];
}
this.state = {
error: this.props.error || undefined,
like: this.props.like || undefined,
copying: false,
keys_to_copy: {}
}
for (const field of props.fields) {
this.state[field.slug] = props[field.slug]
keys_to_copy: {},
...fieldsObj
}
this.handleChange = this.handleChange.bind(this);
@ -157,7 +176,7 @@ class EditForm extends Component {
event.preventDefault();
const like = event.target.checked;
fetch(`/building/${this.props.building_id}/like.json`, {
fetch(`/api/buildings/${this.props.building_id}/like.json`, {
method: 'POST',
headers:{
'Content-Type': 'application/json'
@ -184,7 +203,7 @@ class EditForm extends Component {
event.preventDefault();
this.setState({error: undefined})
fetch(`/building/${this.props.building_id}.json`, {
fetch(`/api/buildings/${this.props.building_id}.json`, {
method: 'POST',
body: JSON.stringify(this.state),
headers:{
@ -353,20 +372,6 @@ class EditForm extends Component {
}
}
EditForm.propTypes = {
title: PropTypes.string,
slug: PropTypes.string,
cat: PropTypes.string,
help: PropTypes.string,
error: PropTypes.object,
like: PropTypes.bool,
building_like: PropTypes.bool,
selectBuilding: PropTypes.func,
building_id: PropTypes.number,
inactive: PropTypes.bool,
fields: PropTypes.array
}
const TextInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
@ -424,7 +429,20 @@ LongTextInput.propTypes = {
handleChange: PropTypes.func
}
class MultiTextInput extends Component {
class MultiTextInput extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func,
copy: PropTypes.bool,
toggleCopyAttribute: PropTypes.func,
copying: PropTypes.bool
};
constructor(props) {
super(props);
this.edit = this.edit.bind(this);
@ -497,19 +515,6 @@ class MultiTextInput extends Component {
}
}
MultiTextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func,
copy: PropTypes.bool,
toggleCopyAttribute: PropTypes.func,
copying: PropTypes.bool
}
const TextListInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
@ -521,7 +526,7 @@ const TextListInput = (props) => (
id={props.slug} name={props.slug}
value={props.value || ''}
disabled={props.disabled}
list={`${props.slug}_suggestions`}
// list={`${props.slug}_suggestions`} TODO: investigate whether this was needed
onChange={props.handleChange}>
<option value="">Select a source</option>
{
@ -571,7 +576,22 @@ NumberInput.propTypes = {
handleChange: PropTypes.func
}
class YearEstimator extends Component {
class YearEstimator extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
date_year: PropTypes.number,
date_upper: PropTypes.number,
date_lower: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func,
copy: PropTypes.bool,
toggleCopyAttribute: PropTypes.func,
copying: PropTypes.bool
};
constructor(props) {
super(props);
this.state = {
@ -594,21 +614,6 @@ class YearEstimator extends Component {
}
}
YearEstimator.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
date_year: PropTypes.number,
date_upper: PropTypes.number,
date_lower: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func,
copy: PropTypes.bool,
toggleCopyAttribute: PropTypes.func,
copying: PropTypes.bool
}
const CheckboxInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
@ -675,7 +680,7 @@ LikeButton.propTypes = {
handleLike: PropTypes.func
}
const Label = (props) => {
const Label: React.SFC<any> = (props) => { // TODO: remove any
return (
<label htmlFor={props.slug}>
{props.title}

View File

@ -46,7 +46,18 @@ BuildingView.propTypes = {
building_like: PropTypes.bool
}
class DataSection extends React.Component {
class DataSection extends React.Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
title: PropTypes.string,
cat: PropTypes.string,
slug: PropTypes.string,
intro: PropTypes.string,
help: PropTypes.string,
inactive: PropTypes.bool,
building_id: PropTypes.number,
children: PropTypes.node
};
constructor(props) {
super(props);
this.state = {
@ -194,18 +205,7 @@ class DataSection extends React.Component {
}
}
DataSection.propTypes = {
title: PropTypes.string,
cat: PropTypes.string,
slug: PropTypes.string,
intro: PropTypes.string,
help: PropTypes.string,
inactive: PropTypes.bool,
building_id: PropTypes.number,
children: PropTypes.node
}
const DataEntry = (props) => {
const DataEntry: React.SFC<any> = (props) => { // TODO: remove any
return (
<Fragment>
<dt>
@ -241,9 +241,9 @@ DataEntry.propTypes = {
value: PropTypes.any
}
const LikeDataEntry = (props) => {
const LikeDataEntry: React.SFC<any> = (props) => { // TODO: remove any
const data_string = JSON.stringify({like: true});
(
return (
<Fragment>
<dt>
{ props.title }
@ -266,7 +266,7 @@ const LikeDataEntry = (props) => {
}
</dd>
{
(props.user_building_like)? <dd>&hellip;including you!</dd> : null
(props.user_building_like)? <dd>&hellip;including you!</dd> : ''
}
</Fragment>
);
@ -280,7 +280,7 @@ LikeDataEntry.propTypes = {
user_building_like: PropTypes.bool
}
const MultiDataEntry = (props) => {
const MultiDataEntry: React.SFC<any> = (props) => { // TODO: remove any
let content;
if (props.value && props.value.length) {

View File

@ -0,0 +1,7 @@
.modal.modal-show {
display: block;
}
.modal.modal-hide {
display: none;
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import './confirmation-modal.css';
interface ConfirmationModalProps {
show: boolean,
title: string,
description: string,
confirmButtonText?: string,
confirmButtonClass?: string,
cancelButtonClass?: string,
onConfirm: () => void,
onCancel: () => void
}
const ConfirmationModal: React.FunctionComponent<ConfirmationModalProps> = ({
confirmButtonText = 'OK',
confirmButtonClass = 'btn-primary',
cancelButtonClass = '',
...props
}) => {
const modalShowClass = props.show ? 'modal-show': 'modal-hide';
return (
<div className={`modal ${modalShowClass}`} tabIndex={-1} role="dialog">
<div className="modal-backdrop">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{props.title}</h5>
<button
type="button"
className="close"
aria-label="Close"
onClick={() => props.onCancel()}
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div className="modal-body">
<p>{props.description}</p>
</div>
<div className="modal-footer">
<button
type="button"
className={`btn btn-block ${confirmButtonClass}`}
onClick={() => props.onConfirm()}
>{confirmButtonText}</button>
<button
type="button"
className={`btn btn-block ${cancelButtonClass}`}
onClick={() => props.onCancel()}
>Cancel</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default ConfirmationModal;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Link } from 'react-router-dom';
import InfoBox from './info-box';
const ContributorAgreementPage : React.SFC<any> = () => (
<article>
<section className='main-col'>
<h1>Contributor Agreement</h1>
<InfoBox msg="This is a draft contributor agreement." />
<h2 className='h2'>Open data</h2>
<p>
Colouring London contributions are open data, licensed under the <a href="http://opendatacommons.org/licenses/odbl/">Open Data Commons Open Database License</a> (ODbL) by Colouring London contributors.
</p>
<p>
You are free to copy, distribute, transmit and adapt our data, as long as you credit Colouring London and our contributors.If you alter or build upon our data, you may distribute the result only under the same licence.
</p>
<h2 className='h2'>Your contributions</h2>
<p>
Colouring London emphasises local and community knowledge. Contributors use a variety of sources and local and expert knowledge of buildings to contribute data and verify that Colouring London is accurate and up to date.
</p>
<p>
When you contribute to Colouring London, you make your contributions available as open data for anyone to copy, distribute, transmit and adapt in line with the licence.
</p>
<p>
We are unable to accept any data derived from copyright or restricted sources, other than as covered by fair use.
</p>
<p>
We encourage full attribution of data sources where appropriate - more details on potential sources are documented with suggestions for each <a href="https://www.pages.colouring.london/buildingcategories">data category</a>.
</p>
<p>
When you make a contribution to Colouring London, you are creating a permanent, public record of all data added, removed, or changed by you.The database records the username and ID of the user making the edit, along with the time and date of the change.All of this information is also made publicly available through the website and through bulk downloads of the edit history.
</p>
<div className="buttons-container">
<Link to="sign-up.html" className="btn btn-outline-dark">Back to sign up</Link>
</div>
</section>
</article>
)
export default ContributorAgreementPage;

View File

@ -109,7 +109,7 @@
"title": "Type",
"slug": "type",
"intro": "How were buildings previously used? Coming soon…",
"help": "https://www.pages.colouring.london/copy-of-type-and-use",
"help": "https://www.pages.colouring.london/buildingtypology",
"fields": [
{
"title": "Original use (as constructed)",
@ -347,54 +347,54 @@
},
{
"inactive": true,
"title": "Energy",
"slug": "energy",
"title": "Sustainability",
"slug": "sustainability",
"intro": "Are buildings energy efficient? Coming soon…",
"help": "https://pages.colouring.london/sustainability",
"fields": [
{
"title": "Energy Performance Certificate (EPC) rating",
"disabled": true,
"slug": "energy_epc_band_current",
"slug": "sustainability_epc_band_current",
"type": "text_list",
"options": ["A", "B", "C", "D", "E", "F", "G"]
},
{
"title": "Display Energy Certificate (DEC)",
"disabled": true,
"slug": "energy_dec_band_current",
"slug": "sustainability_dec_band_current",
"type": "text_list",
"options": ["A", "B", "C", "D", "E", "F", "G"]
},
{
"title": "BREEAM Rating",
"disabled": true,
"slug": "energy_breeam_rating",
"slug": "sustainability_breeam_rating",
"type": "number",
"step": 1
},
{
"title": "Year of last significant retrofit",
"disabled": true,
"slug": "energy_last_retrofit_date",
"slug": "sustainability_last_retrofit_date",
"type": "number",
"step": 1
},
{
"title": "Embodied carbon estimation (for discussion)",
"slug": "energy_embodied_carbon",
"slug": "sustainability_embodied_carbon",
"type": "text",
"disabled": true
},
{
"title": "Adaptability/repairability rating (for discussion)",
"slug": "energy_adaptability_rating",
"slug": "sustainability_adaptability_rating",
"type": "text",
"disabled": true
},
{
"title": "Expected lifespan (for discussion)",
"slug": "energy_expected_lifespan",
"slug": "sustainability_expected_lifespan",
"type": "number",
"step": 1,
"disabled": true
@ -406,7 +406,7 @@
"title": "Greenery",
"slug": "greenery",
"intro": "Is there greenery nearby? Coming soon…",
"help": "https://pages.colouring.london/copy-of-street-context",
"help": "https://pages.colouring.london/greenery",
"fields": [
{
"title": "Gardens"
@ -433,7 +433,7 @@
"title": "Community",
"slug": "community",
"intro": "How does this building work for the local community?",
"help": "https://pages.colouring.london/type",
"help": "https://pages.colouring.london/community",
"fields": [
{
"title": "Is this a publicly owned building?",
@ -456,7 +456,7 @@
"title": "Planning",
"slug": "planning",
"intro": "Planning controls relating to protection and reuse.",
"help": "https://pages.colouring.london/controls",
"help": "https://pages.colouring.london/planning",
"fields": [
{
"title": "Planning portal link",

View File

@ -0,0 +1,84 @@
import React, { FormEvent, ChangeEvent } from 'react';
import InfoBox from './info-box';
import ErrorBox from './error-box';
interface ForgottenPasswordState {
success: boolean;
error: string;
email: string;
emailUsed: string;
}
export default class ForgottenPassword extends React.Component<{}, ForgottenPasswordState> {
constructor(props) {
super(props);
this.state = {
error: undefined,
success: undefined,
email: undefined,
emailUsed: undefined
};
}
handleChange(event: ChangeEvent<HTMLInputElement>) {
const { name, value } = event.currentTarget;
this.setState({ [name]: value } as any);
}
async handleSubmit(event: FormEvent) {
event.preventDefault();
this.setState({ error: undefined, success: undefined });
const emailSent = this.state.email;
try {
const res = await fetch('/api/users/password', {
method: 'PUT',
body: JSON.stringify({ email: emailSent }),
headers: {
'Content-Type': 'application/json'
}
});
const data = await res.json();
if (data.error != undefined) {
this.setState({ error: data.error });
} else if (data.success === true) {
this.setState({ success: true, emailUsed: emailSent});
} else {
this.setState({ error: 'Unexpected result.' });
}
} catch (err) {
this.setState({ error: 'Something went wrong.' });
}
}
render() {
return (
<article>
<section className="main-col">
<h1 className="h2">Forgotten password</h1>
<p>Please provide the e-mail address associated with your account. A password reset link will be sent to your mailbox.</p>
<ErrorBox msg={this.state.error} />
<InfoBox msg="">
{this.state.success ?
`A password reset link has been sent to ${this.state.emailUsed}. Please check your inbox.` :
null
}
</InfoBox>
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="email">E-mail</label>
<input name="email" id="email"
className="form-control" type="email"
placeholder="Your e-mail address" required
onChange={e => this.handleChange(e)}
/>
<div className="buttons-container">
<input type="submit" value="Request password reset" className="btn btn-primary" />
</div>
</form>
</section>
</article>
)
}
}

View File

@ -8,7 +8,13 @@ import './header.css';
/**
* Render the main header using a responsive design
*/
class Header extends React.Component {
class Header extends React.Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
user: PropTypes.shape({
username: PropTypes.string
})
};
constructor(props) {
super(props);
this.state = {collapseMenu: true};
@ -106,10 +112,4 @@ class Header extends React.Component {
}
}
Header.propTypes = {
user: PropTypes.shape({
username: PropTypes.string
})
}
export default Header;

View File

@ -65,8 +65,8 @@ const LEGEND_CONFIG = {
title: 'Team',
elements: []
},
energy: {
title: 'Energy',
sustainability: {
title: 'Sustainability',
elements: []
},
greenery: {
@ -97,7 +97,12 @@ const LEGEND_CONFIG = {
};
class Legend extends React.Component {
class Legend extends React.Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
slug: PropTypes.string,
color: PropTypes.string,
text: PropTypes.string
};
constructor(props) {
super(props);
@ -195,11 +200,4 @@ class Legend extends React.Component {
}
Legend.propTypes = {
slug: PropTypes.string,
color: PropTypes.string,
text: PropTypes.string
}
export default Legend;

View File

@ -6,7 +6,12 @@ import ErrorBox from './error-box';
import InfoBox from './info-box';
import SupporterLogos from './supporter-logos';
class Login extends Component {
class Login extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
login: PropTypes.func,
user: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
@ -34,7 +39,7 @@ class Login extends Component {
event.preventDefault();
this.setState({error: undefined})
fetch('/login', {
fetch('/api/login', {
method: 'POST',
body: JSON.stringify(this.state),
headers:{
@ -47,7 +52,7 @@ class Login extends Component {
if (res.error) {
this.setState({error: res.error})
} else {
fetch('/users/me', {
fetch('/api/users/me', {
credentials: 'same-origin'
}).then(
(res) => res.json()
@ -106,6 +111,8 @@ class Login extends Component {
<label htmlFor="show_password" className="form-check-label">Show password?</label>
</div>
<Link to="/forgotten-password.html">Forgotten password?</Link>
<div className="buttons-container">
<input type="submit" value="Log In" className="btn btn-primary" />
</div>
@ -127,9 +134,4 @@ class Login extends Component {
}
}
Login.propTypes = {
login: PropTypes.func,
user: PropTypes.object
}
export default Login;

View File

@ -16,8 +16,16 @@ const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
/**
* Map area
*/
class ColouringMap extends Component {
class ColouringMap extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
building: PropTypes.object,
revision_id: PropTypes.number,
selectBuilding: PropTypes.func,
colourBuilding: PropTypes.func,
match: PropTypes.object,
history: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
@ -53,7 +61,7 @@ class ColouringMap extends Component {
const newCat = parseCategoryURL(this.props.match.url);
const mapCat = newCat || 'age';
fetch(
'/buildings/locate?lat='+lat+'&lng='+lng
'/api/buildings/locate?lat='+lat+'&lng='+lng
).then(
(res) => res.json()
).then(function(data){
@ -167,13 +175,4 @@ class ColouringMap extends Component {
}
}
ColouringMap.propTypes = {
building: PropTypes.object,
revision_id: PropTypes.number,
selectBuilding: PropTypes.func,
colourBuilding: PropTypes.func,
match: PropTypes.object,
history: PropTypes.object
}
export default ColouringMap;

View File

@ -35,7 +35,7 @@ const MultiEdit = (props) => {
}
const q = parse(props.location.search);
const data = JSON.parse(q.data)
const data = JSON.parse(q.data as string) // TODO: verify what happens when data is string[]
const title = sectionTitleFromCat(cat);
return (
<Sidebar
@ -116,7 +116,7 @@ function fieldTitleFromSlug(slug) {
(prev, section) => {
const el = prev.concat(
section.fields.filter(
field => field.slug === slug
(field: any) => field.slug === slug // TODO: remove any
)
)
return el

View File

@ -1,14 +1,28 @@
import React, { Component } from 'react';
import React, { Component, FormEvent } from 'react';
import { Link, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import ErrorBox from './error-box';
import ConfirmationModal from './confirmation-modal';
class MyAccountPage extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
user: PropTypes.shape({
username: PropTypes.string,
email: PropTypes.string,
registered: PropTypes.instanceOf(Date), // TODO: check if fix correct
api_key: PropTypes.string,
error: PropTypes.object
}),
updateUser: PropTypes.func,
logout: PropTypes.func
};
class MyAccountPage extends Component {
constructor(props) {
super(props);
this.state = {
error: undefined
error: undefined,
showDeleteConfirm: false
};
this.handleLogout = this.handleLogout.bind(this);
this.handleGenerateKey = this.handleGenerateKey.bind(this);
@ -18,7 +32,7 @@ class MyAccountPage extends Component {
event.preventDefault();
this.setState({error: undefined});
fetch('/logout', {
fetch('/api/logout', {
method: 'POST',
credentials: 'same-origin'
}).then(
@ -38,7 +52,7 @@ class MyAccountPage extends Component {
event.preventDefault();
this.setState({error: undefined});
fetch('/api/key', {
fetch('/api/api/key', {
method: 'POST',
credentials: 'same-origin'
}).then(
@ -54,6 +68,37 @@ class MyAccountPage extends Component {
);
}
confirmDelete(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
this.setState({ showDeleteConfirm: true });
}
hideConfirmDelete() {
this.setState({ showDeleteConfirm: false });
}
async handleDelete() {
this.setState({ error: undefined });
try {
const res = await fetch('/api/users/me', {
method: 'DELETE',
credentials: 'same-origin'
});
const data = await res.json();
if(data.error) {
this.setState({ error: data.error });
} else {
this.props.logout();
}
} catch (err) {
this.setState({ error: err });
} finally {
this.hideConfirmDelete();
}
}
render() {
if (this.props.user && !this.props.user.error) {
return (
@ -69,7 +114,7 @@ class MyAccountPage extends Component {
</p>
<ErrorBox msg={this.state.error} />
<form method="POST" action="/logout" onSubmit={this.handleLogout}>
<form onSubmit={this.handleLogout}>
<div className="buttons-container">
<Link to="/edit/age.html" className="btn btn-warning">Start colouring</Link>
<input className="btn btn-secondary" type="submit" value="Log out"/>
@ -92,13 +137,33 @@ class MyAccountPage extends Component {
<p>Are you a software developer? If so, you might be interested in these.</p>
<h3 className="h3">API key</h3>
<p>{this.props.user.api_key? this.props.user.api_key : '-'}</p>
<form method="POST" action="/api/key" onSubmit={this.handleGenerateKey} className="form-group mb-3">
<form onSubmit={this.handleGenerateKey} className="form-group mb-3">
<input className="btn btn-warning" type="submit" value="Generate API key"/>
</form>
<h3 className="h3">GitHub</h3>
<a href="http://github.com/tomalrussell/colouring-london/">Colouring London Github repository</a>
<hr />
<h2 className="h2">Account actions</h2>
<form
onSubmit={e => this.confirmDelete(e)}
className="form-group mb-3"
>
<input className="btn btn-danger" type="submit" value="Delete account" />
</form>
<ConfirmationModal
show={this.state.showDeleteConfirm}
title="Confirm account deletion"
description="Are you sure you want to delete your account? This cannot be undone."
confirmButtonText="Delete account"
confirmButtonClass="btn-danger"
onConfirm={() => this.handleDelete()}
onCancel={() => this.hideConfirmDelete()}
/>
</section>
</article>
);
@ -110,16 +175,4 @@ class MyAccountPage extends Component {
}
}
MyAccountPage.propTypes = {
user: PropTypes.shape({
username: PropTypes.string,
email: PropTypes.string,
registered: PropTypes.date,
api_key: PropTypes.string,
error: PropTypes.object
}),
updateUser: PropTypes.func,
logout: PropTypes.func
}
export default MyAccountPage;

View File

@ -0,0 +1,122 @@
import React, { ChangeEvent, FormEvent } from 'react';
import { RouteComponentProps, Redirect } from 'react-router';
import ErrorBox from './error-box';
import { Link } from 'react-router-dom';
interface PasswordResetState {
error: string;
success: boolean;
token: string;
password: string;
confirmPassword: string;
}
export default class PasswordReset extends React.Component<RouteComponentProps, PasswordResetState> {
static tokenMissingMessage = 'Password reset token is missing. Make sure to follow the link from a password reset email!';
constructor(props) {
super(props);
this.state = {
error: undefined,
success: undefined,
token: undefined,
password: undefined,
confirmPassword: undefined
};
}
componentDidMount() {
const queryParams = new URLSearchParams(this.props.location.search);
const token = queryParams.get('token');
if(token == undefined) {
this.setState({ error: PasswordReset.tokenMissingMessage });
} else {
this.setState({ token: token });
}
}
handleChange(event: FormEvent<HTMLInputElement>) {
const { name, value } = event.currentTarget;
this.setState({
[name]: value,
} as any);
}
async handleSubmit(event: FormEvent) {
event.preventDefault();
this.setState({ error: undefined });
if(this.state.token == undefined) {
this.setState({ error: PasswordReset.tokenMissingMessage });
return;
}
if(this.state.password !== this.state.confirmPassword) {
this.setState({ error: 'Passwords do not match' });
return;
}
const requestData = {
token: this.state.token,
password: this.state.password
};
const res = await fetch('/api/users/password', {
method: 'PUT',
body: JSON.stringify(requestData),
headers: {
'Content-Type': 'application/json'
}
});
const data = await res.json();
if (data.error != undefined) {
this.setState({ error: data.error });
} else if (data.success === true) {
this.setState({ success: true });
} else {
this.setState({ error: 'Unexpected result.' });
}
}
render() {
if (this.state.success) {
return <Redirect to="/my-account.html" />;
}
return (
<article>
<section className="main-col">
<h1 className="h2">Forgotten password</h1>
<p>Please input the new password you want to set for your account. If your password reset token does not work, go back to the <Link to="/forgotten-password.html">forgotten password page</Link> to request a new link.</p>
<ErrorBox msg={this.state.error} />
<form onSubmit={e => this.handleSubmit(e)}>
<label htmlFor="email">New password</label>
<input name="password"
className="form-control" type="password"
placeholder="New password" required
onChange={e => this.handleChange(e)}
/>
<label htmlFor="email">Confirm new password</label>
<input name="confirmPassword"
className="form-control" type="password"
placeholder="Confirm new password" required
onChange={e => this.handleChange(e)}
/>
<input name="token" id="token" type="hidden" value={this.state.token} />
<div className="buttons-container">
<input type="submit" value="Change password" className="btn btn-primary" />
</div>
</form>
</section>
</article>
)
}
}

View File

@ -0,0 +1,121 @@
import React from 'react';
import { Link } from 'react-router-dom';
import InfoBox from './info-box';
const PrivacyPolicyPage: React.SFC<any> = () => (
<article>
<section className='main-col'>
<h1 className='h1'>Colouring London Privacy Policy </ h1>
<InfoBox msg="This is a draft privacy policy." />
<p>
This privacy policy explains how Colouring London uses the personal data we collect from you when you use our website. Colouring London is a research project developed by the Bartlett Centre for Advanced Spatial Analysis (CASA) at UCL. Colouring London is registered for data protection purposes with the UCL data protection office.
</p>
<h2 className='h2'>What data do we collect?</h2>
<p>
Colouring London collects the following personal data:
</p>
<p>
A username and email address. We recommend you do not use your actual name for your username. We also collect your password, which is stored as a cryptographic hash unique to Colouring London.
</p>
<h2 className='h2'>How do we collect your data?</h2>
<p>
You provide Colouring London with a minimal amount of personal data when you register with the website and accepts the terms and conditions including this privacy policy.
</p>
<h2 className='h2'>What purposes do we use your data?</h2>
<p>
Colouring London uses your personal data to enable you to login to access and contribute to the Colouring London project and to provide a personalised user experience when you are logged in. We will not share your personal data (such as your email address) with any other parties or use your personal data for any purposes other than the Colouring London project.
</p>
<h2 className='h2'>What is the legal basis for processing your data?</h2>
<p>
Data protection laws require us to meet certain conditions before we are allowed to use your data in the manner described in this notice, including having a legal basis for the processing. Colouring London, as a research project, is processing your personal data in pursuance of performing a task in the public interest. For further details on the public task legal basis for processing, please see UCLs Statement of Tasks in the Public Interest, available <a href='https://www.ucl.ac.uk/legal-services/sites/legal-services/files/ucl_statement_of_tasks_in_the_public_interest_-_august_2018.pdf'>here</a>
</p>
<h2 className='h2'>How do we store your data?</h2>
<p>
Colouring London stores your data at UCL in London behind the organisations firewall in a secure database using industry standard practices.
</p>
<h2 className='h2'>How do we use cookies?</h2>
<p>
Colouring London only uses cookies to improve the user experience of users of the website, for example we use cookies to keep you signed in. We do not use cookies for marketing or advertising purposes.
</p>
<h2 className='h2'>What are your data protection rights?</h2>
<p>
Under the General Data Protection Regulation, you have certain individual rights in relation to the personal information we hold about you. For the purposes of research where such individual rights would seriously impair research outcomes, such rights are limited. However, subject to certain conditions, you have the following rights in relation to your personal data:
</p>
<ul>
<li>
A right to access personal data held by us about you.
</ li>
<li>
A right to require us to rectify any inaccurate personal data held by us about you.
</ li>
<li>
A right to require us to erase personal data held by us about you. This right will only apply where, for example, we no longer need to use the personal data to achieve the purpose we collected it for.
</ li>
<li>
A right to restrict our processing of personal data held by us about you. This right will only apply where, for example, you dispute the accuracy of the personal data held by us; or where you would have the right to require us to erase the personal data but would prefer that our processing is restricted instead; or where we no longer need to use the personal data to achieve the purpose we collected it for, but we require the data for the purposes of dealing with legal claims.
</ li>
<li>
A right to receive personal data, which you have provided to us, in a structured, commonly used and machine-readable format. You also have the right to require us to transfer this personal data to another organisation.
</ li>
<li>
A right to object to our processing of personal data held by us about you.
</ li>
<li>
A right to withdraw your consent, where we are relying on it to use your personal data.
</ li>
<li>
A right to ask us not to use information about you in a way that allows computers to make decisions about you and ask us to stop.
</ li>
</ul>
<p>
It is important to understand that the extent to which these rights apply to research will vary and that in some circumstances your rights may be restricted. If you notify us (using the contact details set out below) that you wish to exercise any of the above rights and it is considered necessary to refuse to comply with any of your individual rights, you will be informed of the decision within one month and you also have the right to complain about our decision to the Information Commissioners Office.
</p>
<p>
Please also note that we can only comply with a request to exercise your rights during the period for which we hold personal information about you. If that information has been irreversibly anonymised and has become part of the research data set, it will no longer be possible for us to access your personal information.
</p>
<h2 className='h2'>Changes to this privacy policy</h2>
<p>
Changes to this privacy policy will be notified via the Colouring London website. This privacy policy was last updated on 13 August 2019.
</p>
<h2 className='h2'>Who do I contact with questions?</h2>
<p>
If you have any questions about your personal data and Colouring London that are not answered by this privacy notice then please consult UCL's data protection web pages here, where further guidance and relevant UCL policy documentation can be found.
</p>
<p>
If you need further assistance in the first instance, please email <a href='mailto:casa@ucl.ac.uk'>casa@ucl.ac.uk</a>. If you wish to complain about our use of your personal data or exercise any of your rights, please contact UCL's Data Protection Officer: <a href='mailto:data-protection@ucl.ac.uk'>data-protection@ucl.ac.uk</a> or Data Protection Officer, UCL Gower Street, London WC1E 6BT.
</p>
<p>
If we are unable to adequately address any concerns you may have about the way in which we use your data, you have the right to lodge a formal complaint with the UK Information Commissioner's Office. Full details may be accessed on the complaints section of the Information Commissioner's Office <a href='https://ico.org.uk'>website</a>.
</p>
<div className="buttons-container">
<Link to="sign-up.html" className="btn btn-outline-dark">Back to sign up</Link>
</div>
</section>
</article>
)
export default PrivacyPolicyPage;

View File

@ -6,8 +6,12 @@ import { SearchIcon } from './icons';
/**
* Search for location
*/
class SearchBox extends Component {
class SearchBox extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
onLocate: PropTypes.func,
isBuilding: PropTypes.bool
};
constructor(props) {
super(props);
this.state = {
@ -74,7 +78,7 @@ class SearchBox extends Component {
})
fetch(
'/search?q='+this.state.q
'/api/search?q='+this.state.q
).then(
(res) => res.json()
).then((data) => {
@ -156,7 +160,7 @@ class SearchBox extends Component {
: null;
return (
<div className={`search-box ${this.props.isBuilding? 'building' : ''}`} onKeyDown={this.handleKeyPress}>
<form action="/search" method="GET" onSubmit={this.search} className="form-inline">
<form onSubmit={this.search} className="form-inline">
<div onClick={this.state.smallScreen ? this.expandSearch : null}>
<SearchIcon/>
</div>
@ -178,9 +182,4 @@ class SearchBox extends Component {
}
}
SearchBox.propTypes = {
onLocate: PropTypes.func,
isBuilding: PropTypes.bool
}
export default SearchBox;

View File

@ -165,11 +165,11 @@
.section-header.team.active > a::before {
color: #6f879c;
}
.section-header.active.energy {
.section-header.active.sustainability {
border-bottom-color: #5ec232;
}
.section-header.energy:hover > a::before,
.section-header.energy.active > a::before {
.section-header.sustainability:hover > a::before,
.section-header.sustainability.active > a::before {
color: #5ec232;
}
.section-header.active.greenery {

View File

@ -6,7 +6,12 @@ import ErrorBox from './error-box';
import InfoBox from './info-box';
import SupporterLogos from './supporter-logos';
class SignUp extends Component {
class SignUp extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
login: PropTypes.func.isRequired,
user: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
@ -37,7 +42,7 @@ class SignUp extends Component {
event.preventDefault();
this.setState({error: undefined})
fetch('/users', {
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(this.state),
headers:{
@ -50,7 +55,7 @@ class SignUp extends Component {
if (res.error) {
this.setState({error: res.error})
} else {
fetch('/users/me', {
fetch('/api/users/me', {
credentials: 'same-origin'
}).then(
(res) => res.json()
@ -130,9 +135,9 @@ class SignUp extends Component {
onChange={this.handleChange}
required />
<label className="form-check-label" htmlFor="confirm_conditions">
I confirm that I have read and agree to the <a
href="/privacy-policy">privacy policy</a> and <a
href="/user-agreement">contributor agreement</a>.
I confirm that I have read and agree to the <Link
to="/privacy-policy.html">privacy policy</Link> and <Link
to="/contributor-agreement.html">contributor agreement</Link>.
</label>
</div>
@ -157,9 +162,4 @@ class SignUp extends Component {
}
}
SignUp.propTypes = {
login: PropTypes.func.isRequired,
user: PropTypes.object
}
export default SignUp;

View File

@ -4,7 +4,11 @@ import PropTypes from 'prop-types';
import './tooltip.css';
import { InfoIcon } from './icons';
class Tooltip extends Component {
class Tooltip extends Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
text: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
@ -44,8 +48,4 @@ class Tooltip extends Component {
}
}
Tooltip.propTypes = {
text: PropTypes.string
}
export default Tooltip;

View File

@ -10,12 +10,10 @@ const server = http.createServer(app);
let currentApp = app;
server.listen(process.env.PORT || 3000, error => {
if (error) {
console.log(error);
}
server.listen(process.env.PORT || 3000, () => {
console.log('🚀 started');
}).on('error', error => {
console.log(error);
});
// In development mode, enable hot module reloading (HMR)

View File

@ -1,429 +0,0 @@
/**
* Server-side Express application
* - API methods
* - entry-point to shared React App
*
*/
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import express from 'express';
import { renderToString } from 'react-dom/server';
import serialize from 'serialize-javascript';
import bodyParser from 'body-parser';
import session from 'express-session';
import pgConnect from 'connect-pg-simple';
import App from './frontend/app';
import db from './db';
import { authUser, createUser, getUserById, authAPIUser, getNewUserAPIKey } from './api/user';
import {
queryBuildingsAtPoint,
queryBuildingsByReference,
getBuildingById,
getBuildingLikeById,
getBuildingUPRNsById,
saveBuilding,
likeBuilding,
unlikeBuilding
} from './api/building';
import { queryLocation } from './api/search';
import tileserver from './tiles/tileserver';
import { parseBuildingURL } from './parse';
// create server
const server = express();
// reference packed assets
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
// disable header
server.disable('x-powered-by');
// serve static files
server.use(express.static(process.env.RAZZLE_PUBLIC_DIR));
// parse POSTed json body
server.use(bodyParser.json());
// handle user sessions
const pgSession = pgConnect(session);
const sess = {
name: 'cl.session',
store: new pgSession({
pgPromise: db,
tableName: 'user_sessions'
}),
secret: process.env.APP_COOKIE_SECRET,
saveUninitialized: false,
resave: false,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
};
if (server.get('env') === 'production') {
// trust first proxy
server.set('trust proxy', 1)
// serve secure cookies
sess.cookie.secure = true
}
server.use(session(sess));
// handle HTML routes (server-side rendered React)
server.get('/*.html', frontendRoute);
server.get('/', frontendRoute);
function frontendRoute(req, res) {
const context = {};
const data = {};
context.status = 200;
const userId = req.session.user_id;
const buildingId = parseBuildingURL(req.url);
const isBuilding = (typeof (buildingId) !== 'undefined');
if (isBuilding && isNaN(buildingId)) {
context.status = 404;
}
Promise.all([
userId ? getUserById(userId) : undefined,
isBuilding ? getBuildingById(buildingId) : undefined,
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false
]).then(function (values) {
const user = values[0];
const building = values[1];
const uprns = values[2];
const buildingLike = values[3];
if (isBuilding && typeof (building) === 'undefined') {
context.status = 404
}
data.user = user;
data.building = building;
data.building_like = buildingLike;
if (data.building != null) {
data.building.uprns = uprns;
}
renderHTML(context, data, req, res)
}).catch(error => {
console.error(error);
data.user = undefined;
data.building = undefined;
data.building_like = undefined;
context.status = 500;
renderHTML(context, data, req, res);
});
}
function renderHTML(context, data, req, res) {
const markup = renderToString(
<StaticRouter context={context} location={req.url}>
<App user={data.user} building={data.building} building_like={data.building_like} />
</StaticRouter>
);
if (context.url) {
res.redirect(context.url);
} else {
res.status(context.status).send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title>Colouring London</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@font-face {
font-family: 'glacial_cl';
src: url('/fonts/glacialindifference-regular-webfont.woff2') format('woff2'),
url('/fonts/glacialindifference-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
</style>
${
assets.client.css
? `<link rel="stylesheet" href="${assets.client.css}">`
: ''
}
${
process.env.NODE_ENV === 'production'
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${assets.client.js}" defer crossorigin></script>`
}
</head>
<body>
<div id="root">${markup}</div>
<script>
window.__PRELOADED_STATE__ = ${serialize(data)}
</script>
</body>
</html>`
);
}
}
// GET tiles
server.use('/tiles', tileserver);
// GET buildings
// not implemented - may be useful to GET all buildings, paginated
// GET buildings at point
server.get('/buildings/locate', function (req, res) {
const { lng, lat } = req.query;
queryBuildingsAtPoint(lng, lat).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
});
// GET buildings by reference (UPRN/TOID or other identifier)
server.get('/buildings/reference', function (req, res) {
const { key, id } = req.query;
queryBuildingsByReference(key, id).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
});
// GET individual building, POST building updates
server.route('/building/:building_id.json')
.get(function (req, res) {
const { building_id } = req.params;
getBuildingById(building_id).then(function (result) {
res.send(result);
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
})
.post(function (req, res) {
if (req.session.user_id) {
updateBuilding(req, res, req.session.user_id);
} else if (req.query.api_key) {
authAPIUser(req.query.api_key)
.then(function (user) {
updateBuilding(req, res, user.user_id)
})
.catch(function (err) {
console.error(err);
res.send({ error: 'Must be logged in' });
});
} else {
res.send({ error: 'Must be logged in' });
}
})
function updateBuilding(req, res, userId) {
const { building_id } = req.params;
const building = req.body;
saveBuilding(building_id, building, userId).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
}
// GET building UPRNs
server.get('/building/:building_id/uprns.json', function (req, res) {
const { building_id } = req.params;
getBuildingUPRNsById(building_id).then(function (result) {
if (typeof (result) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send({
uprns: result
});
}).catch(function (error) {
console.error(error);
res.send({ error: 'Database error' })
})
})
// GET/POST like building
server.route('/building/:building_id/like.json')
.get(function (req, res) {
if (!req.session.user_id) {
res.send({ like: false }); // not logged in, so cannot have liked
return
}
const { building_id } = req.params;
getBuildingLikeById(building_id, req.session.user_id).then(like => {
// any value returned means like
res.send({ like: like })
}).catch(
() => res.send({ error: 'Database error' })
)
})
.post(function (req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
const { building_id } = req.params;
const { like } = req.body;
if (like) {
likeBuilding(building_id, req.session.user_id).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
} else {
unlikeBuilding(building_id, req.session.user_id).then(building => {
if (building.error) {
res.send(building)
return
}
if (typeof (building) === 'undefined') {
res.send({ error: 'Database error' })
return
}
res.send(building)
}).catch(
() => res.send({ error: 'Database error' })
)
}
})
// POST new user
server.post('/users', function (req, res) {
const user = req.body;
if (req.session.user_id) {
res.send({ error: 'Already signed in' });
return
}
if (user.email) {
if (user.email != user.confirm_email) {
res.send({ error: 'Email did not match confirmation.' });
return
}
} else {
user.email = null;
}
createUser(user).then(function (result) {
if (result.user_id) {
req.session.user_id = result.user_id;
res.send({ user_id: result.user_id });
} else {
req.session.user_id = undefined;
res.send({ error: result.error });
}
}).catch(function (err) {
console.error(err);
res.send(err)
});
});
// POST user auth
server.post('/login', function (req, res) {
authUser(req.body.username, req.body.password).then(function (user) {
if (user.user_id) {
req.session.user_id = user.user_id;
} else {
req.session.user_id = undefined;
}
res.send(user);
}).catch(function (error) {
res.send(error);
})
});
// POST user logout
server.post('/logout', function (req, res) {
req.session.user_id = undefined;
req.session.destroy(function (err) {
if (err) {
console.error(err);
res.send({ error: 'Failed to end session' })
}
res.send({ success: true });
});
});
// GET own user info
server.get('/users/me', function (req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
getUserById(req.session.user_id).then(function (user) {
res.send(user);
}).catch(function (error) {
res.send(error);
});
});
// POST generate API key
server.post('/api/key', function (req, res) {
if (!req.session.user_id) {
res.send({ error: 'Must be logged in' });
return
}
getNewUserAPIKey(req.session.user_id).then(function (apiKey) {
res.send(apiKey);
}).catch(function (error) {
res.send(error);
});
})
// GET search
server.get('/search', function (req, res) {
const searchTerm = req.query.q;
if (!searchTerm) {
res.send({
error: 'Please provide a search term'
})
return
}
queryLocation(searchTerm).then((results) => {
if (typeof (results) === 'undefined') {
res.send({
error: 'Database error'
})
return
}
res.send({
results: results.map(item => {
// map from DB results to GeoJSON Feature objects
const geom = JSON.parse(item.st_asgeojson)
return {
type: 'Feature',
attributes: {
label: item.search_str,
zoom: item.zoom || 9
},
geometry: geom
}
})
})
}).catch(function (error) {
res.send(error);
});
})
export default server;

164
app/src/server.tsx Normal file
View File

@ -0,0 +1,164 @@
/**
* Server-side Express application
* - API methods
* - entry-point to shared React App
*
*/
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import express from 'express';
import { renderToString } from 'react-dom/server';
import serialize from 'serialize-javascript';
import session from 'express-session';
import pgConnect from 'connect-pg-simple';
import App from './frontend/app';
import db from './db';
import { getUserById } from './api/services/user';
import {
getBuildingById,
getBuildingLikeById,
getBuildingUPRNsById
} from './api/services/building';
import tileserver from './tiles/tileserver';
import apiServer from './api/api';
import { parseBuildingURL } from './parse';
// create server
const server = express();
// reference packed assets
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
// disable header
server.disable('x-powered-by');
// serve static files
server.use(express.static(process.env.RAZZLE_PUBLIC_DIR));
// handle user sessions
const pgSession = pgConnect(session);
const sess: any = { // TODO: remove any
name: 'cl.session',
store: new pgSession({
pgPromise: db,
tableName: 'user_sessions'
}),
secret: process.env.APP_COOKIE_SECRET,
saveUninitialized: false,
resave: false,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
};
if (server.get('env') === 'production') {
// trust first proxy
server.set('trust proxy', 1)
// serve secure cookies
sess.cookie.secure = true
}
server.use(session(sess));
// handle HTML routes (server-side rendered React)
server.get('/*.html', frontendRoute);
server.get('/', frontendRoute);
function frontendRoute(req, res) {
const context: any = {}; // TODO: remove any
const data: any = {}; // TODO: remove any
context.status = 200;
const userId = req.session.user_id;
const buildingId = parseBuildingURL(req.url);
const isBuilding = (typeof (buildingId) !== 'undefined');
if (isBuilding && isNaN(buildingId)) {
context.status = 404;
}
Promise.all([
userId ? getUserById(userId) : undefined,
isBuilding ? getBuildingById(buildingId) : undefined,
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false
]).then(function (values) {
const user = values[0];
const building = values[1];
const uprns = values[2];
const buildingLike = values[3];
if (isBuilding && typeof (building) === 'undefined') {
context.status = 404
}
data.user = user;
data.building = building;
data.building_like = buildingLike;
if (data.building != null) {
data.building.uprns = uprns;
}
renderHTML(context, data, req, res)
}).catch(error => {
console.error(error);
data.user = undefined;
data.building = undefined;
data.building_like = undefined;
context.status = 500;
renderHTML(context, data, req, res);
});
}
function renderHTML(context, data, req, res) {
const markup = renderToString(
<StaticRouter context={context} location={req.url}>
<App user={data.user} building={data.building} building_like={data.building_like} />
</StaticRouter>
);
if (context.url) {
res.redirect(context.url);
} else {
res.status(context.status).send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title>Colouring London</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@font-face {
font-family: 'glacial_cl';
src: url('/fonts/glacialindifference-regular-webfont.woff2') format('woff2'),
url('/fonts/glacialindifference-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
</style>
${
assets.client.css
? `<link rel="stylesheet" href="${assets.client.css}">`
: ''
}
${
process.env.NODE_ENV === 'production'
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${assets.client.js}" defer crossorigin></script>`
}
</head>
<body>
<div id="root">${markup}</div>
<script>
window.__PRELOADED_STATE__ = ${serialize(data)}
</script>
</body>
</html>`
);
}
}
server.use('/tiles', tileserver);
server.use('/api', apiServer);
// use the frontend route for anything else - will presumably show the 404 page
server.use(frontendRoute);
export default server;

16
app/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"outDir": "./build",
"allowJs": true,
"target": "es2017",
"jsx": "react",
"lib": ["es6", "es2015", "es2017", "dom"],
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"noImplicitAny": false
},
"include": [
"./src/**/*"
]
}

5
app/tslint.json Normal file
View File

@ -0,0 +1,5 @@
{
"rules": {
}
}

View File

@ -21,7 +21,11 @@ module.exports = {
PGUSER: "username",
PGPASSWORD: "longrandomsecret",
APP_COOKIE_SECRET: "longrandomsecret",
MAIL_SERVER_HOST: "mail_hostname",
MAIL_SERVER_PORT: 587,
MAIL_SERVER_USER: "mail_username",
MAIL_SERVER_PASSWORD: "longrandompassword",
WEBAPP_ORIGIN: "http://localhost:3000",
}
}
]

View File

@ -17,7 +17,12 @@ module.exports = {
PGUSER: "username",
PGPASSWORD: "longrandomsecret",
APP_COOKIE_SECRET: "longrandomsecret",
TILECACHE_PATH: "/path/to/tile/cache"
TILECACHE_PATH: "/path/to/tile/cache",
MAIL_SERVER_HOST: "mail_hostname",
MAIL_SERVER_PORT: 587,
MAIL_SERVER_USER: "mail_username",
MAIL_SERVER_PASSWORD: "longrandompassword",
WEBAPP_ORIGIN: "https://beta.colouring.london",
}
}
]

View File

@ -59,7 +59,7 @@ def save_data(building_id, data, api_key, base_url):
"""Save data to a building
"""
r = requests.post(
"{}/building/{}.json?api_key={}".format(base_url, building_id, api_key),
"{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key),
json=data
)

View File

@ -89,7 +89,7 @@ def save_data(building_id, data, api_key, base_url):
"""Save data to a building
"""
r = requests.post(
"{}/building/{}.json?api_key={}".format(base_url, building_id, api_key),
"{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key),
json=data
)

View File

@ -0,0 +1,5 @@
ALTER TABLE users DROP COLUMN IF EXISTS is_deleted;
ALTER TABLE users DROP COLUMN IF EXISTS deleted_on;
DROP INDEX IF EXISTS users_username_idx;
ALTER TABLE users ADD CONSTRAINT users_username_key UNIQUE (username);

View File

@ -0,0 +1,5 @@
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_deleted boolean NOT NULL DEFAULT(false);
ALTER TABLE users ADD COLUMN IF NOT EXISTS deleted_on timestamp NULL;
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_key;
CREATE UNIQUE INDEX users_username_idx ON users (username) WHERE NOT is_deleted;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS user_password_reset_tokens;

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS user_password_reset_tokens (
token uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid,
expires_on timestamp NOT NULL,
used boolean NOT NULL DEFAULT false
)

View File

@ -0,0 +1,28 @@
-- Remove sustainability fields, update in paralell with adding new fields
-- BREEAM rating
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_breeam_rating;
-- BREEAM date
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_breeam_date;
-- DEC (display energy certifcate, only applies to non domestic buildings)
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec;
-- DEC date
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec_date;
--DEC certifcate lmk key, this would be lmkkey, no online lookup but can scrape through API. Numeric (25)
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec_lmkey;
-- Aggregate EPC rating (Estimated) for a building, derived from inidividual certificates
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_aggregate_estimate_epc;
-- Last significant retrofit date YYYY
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_retrofit_date;
--How much embodied carbon? One for ML, tons CO2 int
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_embodied_carbon;
--Life expectancy of the building, via further analysis
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_life_expectancy;
--Average lifespan of typology based on statistical analysis of similar stock
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_lifespan_average;

View File

@ -0,0 +1,67 @@
-- BREEAM ratings, one of:
-- - Outstanding
-- - Excellent
-- - Very good
-- - Good
-- - Pass
-- - Unclassified
CREATE TYPE sust_breeam_rating
AS ENUM ('Outstanding',
'Excellent',
'Very good',
'Good',
'Pass',
'Unclassified');
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_breeam_rating sust_breeam_rating DEFAULT 'Unclassified';
-- Date of BREEAM
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_breeam_date smallint;
-- DEC (display energy certifcate, only applies to non domestic buildings)
-- A - G
CREATE TYPE sust_dec
AS ENUM ('A',
'B',
'C',
'D',
'E',
'F',
'G');
-- Date of DEC YYYY
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_dec_date smallint;
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_dec sust_dec;
-- Aggregate EPC rating (Estimated) for a building, derived from inidividual certificates
-- A+ - G
CREATE TYPE sust_aggregate_estimate_epc
AS ENUM ('A',
'B',
'C',
'D',
'E',
'F',
'G');
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_aggregate_estimate_epc sust_aggregate_estimate_epc;
-- Last significant retrofit date YYYY
ALTER TABLE buildings
ADD COLUMN IF NOT EXISTS sust_retrofit_date smallint;
--How much embodied carbon? One for ML, tons CO2 int
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS sust_embodied_carbon numeric(7,2);
--Life expectancy of the building, via further analysis
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS sust_life_expectancy smallint;
--Average lifespan of typology based on statistical analysis of similar stock
ALTER TABLE buildings ADD COLUMN IF NOTE EXISTS sust_lifespan_average smallint;

View File

@ -0,0 +1,2 @@
--For landuse classifications there are tables containing the landuses, these are stored in a new schema
DROP SCHEMA IF EXISTS reference_tables CASCADE;

View File

@ -0,0 +1,838 @@
--For landuse classifications there are tables containing the landuses, these are stored in a new schema
CREATE SCHEMA IF NOT EXISTS reference_tables;
--Then create the table for landuse
CREATE TABLE IF NOT EXISTS reference_tables.landuse_classifications (
landuse_id VARCHAR(9) NOT NULL,
description VARCHAR(74) NOT NULL,
level VARCHAR(5) NOT NULL,
parent_id VARCHAR(4),
is_used BOOLEAN DEFAULT True
);
--populate with data
--These are taking from the NLUD calssifcations as url below, accessed 12th August 2019
-- https://land.copernicus.eu/eagle/files/eagle-related-projects/22_NLUD_v44.pdf
-- The is_used column is set based on whether we plan to use a given order/class for urban buildings.
-- Groups as a whole are currently unused.
INSERT INTO reference_tables.landuse_classifications
(landuse_id, description, level, parent_id, is_used)
VALUES
-- order
('U010','Agriculture And Fisheries','order',NULL,True),
('U080','Community Services','order',NULL,True),
('U120','Defence','order',NULL,True),
('U020','Forestry','order',NULL,False),
('U100','Industry And Business','order',NULL,True),
('U030','Minerals','order',NULL,False),
('U040','Recreation And Leisure','order',NULL,True),
('U070','Residential','order',NULL,True),
('U090','Retail','order',NULL,True),
('U050','Transport','order',NULL,True),
('U130','Unused Land','order',NULL,False),
('U060','Utilities And Infrastructure','order',NULL,True),
('U110','Vacant And Derelict','order',NULL,True),
--We have created a landuse order that does not exist for mixed use
('U140','Mixed use','order',NULL,True),
-- group
('U011','Agriculture','group','U010', False),
('U012','Fisheries','group','U010', False),
('U021','Managed forest','group','U020', False),
('U022','Un-managed forest','group','U020', False),
('U031','Mineral workings and quarries','group','U030', False),
('U041','Outdoor amenity and open spaces','group','U040', False),
('U042','Amusement and show places','group','U040', False),
('U043','Libraries, museums and galleries','group','U040', False),
('U044','Sports facilities and grounds','group','U040', False),
('U045','Holiday parks and camps','group','U040', False),
('U046','Allotments and city farms','group','U040', False),
('U051','Transport tracks and ways','group','U050', False),
('U054','Transport terminals and interchanges','group','U050', False),
('U053','Car parks','group','U050', False),
('U051','Vehicle storage','group','U050', False),
('U055','Goods and freight terminals','group','U050', False),
('U056','Waterways','group','U050', False),
('U061','Energy production and distribution','group','U060', False),
('U062','Water storage and treatment','group','U060', False),
('U063','Refuse disposal','group','U060', False),
('U064','Cemeteries and crematoria','group','U060', False),
('U065','Post and telecommunications','group','U060', False),
('U071','Dwellings','group','U070', False),
('U072','Hotels, boarding and guest houses','group','U070', False),
('U073','Residential institutions','group','U070', False),
('U081','Medical and health care services','group','U080', False),
('U082','Places of worship','group','U080', False),
('U083','Education','group','U080', False),
('U084','Community services','group','U080', False),
('U091','Shops','group','U090', False),
('U092','Financial and professional services','group','U090', False),
('U093','Restaurants and cafes','group','U090', False),
('U094','Public houses, bars and nightclubs','group','U090', False),
('U101','Manufacturing','group','U100', False),
('U102','Offices','group','U100', False),
('U103','Storage','group','U100', False),
('U104','Wholesale distribution','group','U100', False),
('U111','Vacant','group','U110', False),
('U112','Derelict','group','U110', False),
('U121','Defence','group','U120', False),
('U131','Unused land','group','U130', False),
-- class
('AG05A -A','Agricultural research establishment','class','U011',True),
('AG','Agriculture and fisheries places','class','U011',False),
('AG01A -A','Animal artificial insemination centre','class','U011',False),
('AG01B -A','Animal boarding establishment','class','U011',False),
('AG02A','Animal breeding places','class','U011',True),
('AG01B -B','Animal clipping and manicure establishment','class','U011',True),
('AG01A -B','Animal dipping place','class','U011',True),
('AG02','Animal living places','class','U011',True),
('AG03A','Animal milking places','class','U011',True),
('AG03','Animal product processing places','class','U011',True),
('AG03F -A','Animal product waste store','class','U011',True),
('AG03F -B','Animal product waste treatment places','class','U011',True),
('AG03F','Animal products waste handling places','class','U011',True),
('AG02B','Animal rearing places','class','U011',True),
('AG01A','Animal service places','class','U011',True),
('AG01','Animal service places','class','U011',True),
('AG03B','Animal shearing places','class','U011',True),
('AG03C','Animal slaughtering places','class','U011',True),
('AG03C-A','Animal stunning and killing places','class','U011',True),
('AG01B','Animal welfare places','class','U011',True),
('AG06A','Arable farm places','class','U011',True),
('AG02B -A','Bedding and waste removal','class','U011',True),
('AG01A -C','Blacksmith premises','class','U011',True),
('AG06A -A','Cereal crops','class','U011',False),
('AG07A -A','Cereal crops store','class','U011',True),
('AG07A','Crop conditioning grading and storage places','class','U011',True),
('AG07','Crop processing places','class','U011',True),
('AG06','Cultivated places','class','U011',True),
('AG03D-A','Egg grading place','class','U011',True),
('AG06A -B','Fallow land','class','U011',False),
('AG03E','Feed handling places','class','U011',True),
('AG03E -A','Feed preparation place','class','U011',True),
('AG03E -B','Feed storage place','class','U011',True),
('AG02B -B','Feeding','class','U011',True),
('AG03D-B','Fish sorting place','class','U011',True),
('AG06B -B','Flower bed','class','U011',False),
('AG07A -B','Fruit crops','class','U011',False),
('AG06B -C','Glass house','class','U011',True),
('AG08A','Grazing places','class','U011',False),
('AG06A -C','Green forage crops place','class','U011',False),
('AG07A -C','Green forage crops store','class','U011',False),
('AG06B -D','Hop field','class','U011',False),
('AG06B','Horticultural places','class','U011',True),
('AG02B -C','Isolation (animal living place)','class','U011',False),
('AG03C-B','Knackering place','class','U011',True),
('AG03A -A','Milk extraction place','class','U011',True),
('AG03A -B','Milk treatment place','class','U011',True),
('AG07A -F','Mill and mix building','class','U011',True),
('AG06B -E','Mixed market garden','class','U011',False),
('AG08','Non cultivated places','class','U011',False),
('AG06B -F','Nursery','class','U011',False),
('AG06B -G','Orchard with arable land','class','U011',False),
('AG06B -H','Orchard with grass','class','U011',False),
('AG06B -I','Orchard with market garden','class','U011',False),
('AG07A -G','Packing and sorting (crop processing place)','class','U011',True),
('AG03D','Packing places (animal products)','class','U011',True),
('AG02A -C','Parturition place','class','U011',True),
('AG08A -A','Permanent pasture','class','U011',False),
('AG03C-C','Processing place (animal slaughtering place)','class','U011',True),
('AG06A -D','Pulse crops place','class','U011',False),
('AG07A -D','Pulse crops store','class','U011',True),
('AG01B -C','Quarantine place, animal','class','U011',True),
('AG06A -E','Root crops place','class','U011',True),
('AG07A -E','Root crops store','class','U011',True),
('AG08A -B','Rough grazing','class','U011',False),
('AG02A -D','Service pen','class','U011',False),
('AG06B -J','Soft fruit place','class','U011',False),
('AG07A -H','Vegetable (crop conditioning and storage)','class','U011',True),
('AG06B -K','Vegetable field','class','U011',False),
('AG01B -D','Veterinary hospital','class','U011',True),
('AG01B -E','Veterinary surgery','class','U011',True),
('AG02B -E','Weighing place','class','U011',True),
('AG03B -A','Wool grading place','class','U011',True),
('AG03B -B','Wool removal place','class','U011',True),
('AG02A -A','Fish farm','class','U012',True),
('AG04A','Fishery places','class','U012',True),
('AG02A -B','Hatchery','class','U012',False),
('AG04A -A','Net fishery place','class','U012',True),
('AG04A -B','Pot and other inshore or estuarial fishing place','class','U012',True),
('AG02B -D','Rearing pond','class','U012',False),
('AG04A -C','Rod and line fishery place','class','U012',True),
('AG04','Wild life capturing places','class','U012',True),
('MA06A -A','Abrasives and other building materials manufacturing place','class','U101',True),
('MA04E -A','Aerospace equipment manufacturing or repairing place','class','U101',True),
('MA04A -A','Agricultural machinery manufacturing place','class','U101',True),
('MA01B -A','Aluminium and aluminium alloy manufacturing','class','U101',True),
('MA02A -A','Animal and poultry food manufacturing place','class','U101',True),
('MA02A -B','Bacon curing, meat and fish product manufacturing place','class','U101',True),
('MA07A -A','Bedding and soft furnishings manufacturing place','class','U101',True),
('MA02A -C','Biscuit manufacturing','class','U101',True),
('MA04F -D','Bolts, nuts, screws, rivets etc. manufacturing place','class','U101',True),
('MA02A -D','Bread and flour confectionery manufacturing place','class','U101',True),
('MA02A -E','Brewery','class','U101',True),
('MA06A -B','Bricks, fireclay and refractory goods manufacturing place','class','U101',True),
('MA06A','Bricks, pottery, glass, cement manufacturing places','class','U101',True),
('MA04C-A','Broadcast receiving and sound reproducing equipment manufacturing','class','U101',True),
('MA08A -A','Brushes and brooms manufacturing place','class','U101',True),
('MA04F -A','Can and metal box manufacturing place','class','U101',True),
('MA05A -A','Carpet manufacturing place','class','U101',True),
('MA06A -C','Cement manufacturing place','class','U101',True),
('MA03A','Chemical and allied industries manufacturing places','class','U101',True),
('MA05C','Clothing and footwear manufacturing places','class','U101',True),
('MA05','Clothing, textiles, leather, footwear and fur goods manufacturing places','class','U101',True),
('MA01A','Coal and petroleum processing places','class','U101',True),
('MA01','Coal, oil and metal processing places','class','U101',True),
('MA02A -F','Cocoa, chocolate and sugar confectionery manufacturing place','class','U101',True),
('MA01A -A','Coke ovens and solid fuel manufacturing place','class','U101',True),
('MA09A -A','Construction and demolition site','class','U101',True),
('MA04A -B','Construction and earth moving equipment manufacturing place','class','U101',True),
('MA09','Construction places','class','U101',True),
('MA09A','Construction places','class','U101',True),
('MA01B -B','Copper, brass and other copper alloy manufacturing place','class','U101',True),
('MA04F -B','Cutlery and plated tableware manufacturing place','class','U101',True),
('MA05C-A','Dresses, lingerie, infants wear etc. manufacturing place','class','U101',True),
('MA03A -A','Dyestuffs and pigments manufacturing place','class','U101',True),
('MA04C-B','Electric appliances primarily for domestic use manufacturing places','class','U101',True),
('MA04C','Electrical engineering places','class','U101',True),
('MA04C-C','Electrical machinery manufacturing place','class','U101',True),
('MA04C-D','Electronic computers manufacturing place','class','U101',True),
('MA04','Engineering places','class','U101',True),
('MA03A -B','Explosives and fireworks manufacturing place','class','U101',True),
('MA03A -C','Fertilizer manufacturing place','class','U101',True),
('MA02A','Food, drink and tobacco manufacturing place','class','U101',True),
('MA05C-B','Footwear manufacturing place','class','U101',True),
('MA02A -G','Fruit and vegetable product manufacturing place','class','U101',True),
('MA05B -A','Fur goods manufacturing place','class','U101',True),
('MA07A -B','Furniture and upholstery manufacturing place','class','U101',True),
('MA03A -D','General chemical manufacturing place','class','U101',True),
('MA06A -D','Glass manufacturing place','class','U101',True),
('MA02A -H','Grain mill','class','U101',True),
('MA05C-C','Hats, cap and millinery manufacturing place','class','U101',True),
('MA05A -B','Hosiery and other knitted goods manufacturing place','class','U101',True),
('MA10A -A','Industrial research laboratory','class','U101',True),
('MA04A -C','Industrial services equipment manufacturing place','class','U101',True),
('MA04B','Instrument engineering places','class','U101',True),
('MA04C-E','Insulated wires and cables manufacturing place','class','U101',True),
('MA01B -C','Iron and steel manufacturing place','class','U101',True),
('MA04F -C','Jewellery and precious metal manufacturing place','class','U101',True),
('MA05A -C','Jute manufacturing place','class','U101',True),
('MA05A -B','Knitted goods manufacturing place','class','U101',True),
('MA05A -D','Lace manufacturing place','class','U101',True),
('MA01B -D','Lead manufacturing place','class','U101',True),
('MA05B -C','Leather (tanning and dressing) and fellmongery place','class','U101',True),
('MA05B','Leather and fur goods manufacturing places','class','U101',True),
('MA05B -B','Leather goods manufacturing place','class','U101',True),
('MA08A -B','Linoleum, plastics floor covering, leather cloth manufacturing place','class','U101',True),
('MA04E -B','Locomotives and railway track equipment manufacturing place','class','U101',True),
('MA01A -B','Lubricating oil and grease manufacturing place','class','U101',True),
('MA05A -E','Made up textile manufacturing place','class','U101',True),
('MA05A -F','Man made fibre production manufacturing place','class','U101',True),
('MA10A','Manufacturing research establishments','class','U101',True),
('MA12A -A','Manufacturing storage place','class','U101',True),
('MA12A','Manufacturing storage places','class','U101',True),
('MA11A','Manufacturing waste disposal places','class','U101',True),
('MA11A -A','Manufacturing waste tip','class','U101',True),
('MA04A','Mechanical engineering places','class','U101',True),
('MA04A -D','Mechanical handling equipment manufacturing place','class','U101',True),
('MA01B','Metal processing places (basic forms)','class','U101',True),
('MA04A -E','Metal working machine tools manufacturing place','class','U101',True),
('MA02A -I','Milk and milk product manufacturing place','class','U101',True),
('MA01A -C','Mineral oil refinery','class','U101',True),
('MA08A -C','Miscellaneous goods manufacturing place','class','U101',True),
('MA08A -D','Miscellaneous stationers goods manufacturing place','class','U101',True),
('MA07A -C','Miscellaneous wood and cork manufacturing place','class','U101',True),
('MA04E -C','Motor cycle, tricycle and pedal cycle manufacturing place','class','U101',True),
('MA04E -D','Motor vehicle manufacturing place','class','U101',True),
('MA05A -G','Narrow fabric manufacturing place','class','U101',True),
('MA04A -F','Office machinery manufacturing place','class','U101',True),
('MA04A -G','Ordnance and small arms manufacturing place','class','U101',True),
('MA05C-D','Overalls and men''s shirts and underwear manufacturing place','class','U101',True),
('MA07B -B','Packaging products of paper and associated materials manufacturing place','class','U101',True),
('MA03A -E','Paint manufacturing place','class','U101',True),
('MA07B -C','Paper and board manufacturing place','class','U101',True),
('MA07B','Paper, printing and publishing works','class','U101',True),
('MA07B -D','Periodical and newspaper printing and publishing works','class','U101',True),
('MA03A -F','Pharmaceutical chemicals and preparation manufacturing place','class','U101',True),
('MA04B -A','Photographic and document copying equipment manufacturing place','class','U101',True),
('MA06A -E','Pottery manufacturing place','class','U101',True),
('MA04A -H','Prime movers manufacturing place','class','U101',True),
('MA04A -I','Pumps, valves and compressor manufacturing place','class','U101',True),
('MA04C-F','Radio and electronic capital goods manufacturing place','class','U101',True),
('MA04C-G','Radio, radar and electronic capital goods manufacturing place','class','U101',True),
('MA04E -E','Railway carriages and wagons and trams manufacturing place','class','U101',True),
('MA05A -H','Rope, twine and net manufacturing place','class','U101',True),
('MA08A -E','Rubber goods manufacturing place','class','U101',True),
('MA04B -C','Scientific and industrial instruments and systems manufacturing','class','U101',True),
('MA04D-A','Shipbuilding and marine engineering place','class','U101',True),
('MA07A -D','Shop and office fittings manufacturing place','class','U101',True),
('MA04F -E','Small tools, implements and gauges manufacturing place','class','U101',True),
('MA03A -G','Soap, detergent and fat splitting and distillation manufacturing place','class','U101',True),
('MA02A -J','Soft drinks manufacturing place','class','U101',True),
('MA05A -I','Spinning and doubling (cotton and flax systems) manufacturing place','class','U101',True),
('MA07B -A','Stationery manufacturing place','class','U101',True),
('MA02A -K','Sugar refinery','class','U101',True),
('MA04B -B','Surgical instruments and appliances manufacturing place','class','U101',True),
('MA03A -H','Synthetic resins, plastics and synthetic rubber manufacturing place','class','U101',True),
('MA05C-E','Tailored outerwear manufacturing place','class','U101',True),
('MA04C-H','Telegraph and telephone apparatus and equipment manufacturing place','class','U101',True),
('MA05A -J','Textile finishing place','class','U101',True),
('MA04A -J','Textile machinery and accessories manufacturing place','class','U101',True),
('MA05A','Textile manufacturing places','class','U101',True),
('MA07A','Timber and furniture works','class','U101',True),
('MA07','Timber furniture, paper, printing and publishing works','class','U101',True),
('MA07A -E','Timber works','class','U101',True),
('MA02A -L','Tobacco manufacturing place','class','U101',True),
('MA03A -I','Toilet preparation manufacturing place','class','U101',True),
('MA08A -F','Toys, games, children''s carriages and sports equipment manufacturing place','class','U101',True),
('MA02A -M','Vegetable, animal oil and fat manufacturing place','class','U101',True),
('MA04E','Vehicle engineering places','class','U101',True),
('MA04B -D','Watches and clocks manufacturing place','class','U101',True),
('MA05C-F','Weatherproof outerwear manufacturing place','class','U101',True),
('MA05A -K','Weaving of cotton, linen and man made fibres manufacturing place','class','U101',True),
('MA04E -F','Wheeled tractor manufacturing place','class','U101',True),
('MA04F -F','Wire manufacturing place','class','U101',True),
('MA07A -F','Wooden containers and baskets manufacturing place','class','U101',True),
('MA05A -L','Woollen and worsted manufacturing place','class','U101',True),
('OF03A -A','Business discussion places','class','U102',True),
('OF03A','Business meeting places','class','U102',True),
('OF03','Business meeting places','class','U102',True),
('OF01A -A','Central government administration office','class','U102',True),
('OF01A','General offices','class','U102',True),
('OF01A -B','Local government administration office','class','U102',True),
('OF01A -C','Manufacturing administration office','class','U102',True),
('OF','Offices','class','U102',True),
('OF01A -D','Professional services office','class','U102',True),
('OF04A -A','Studio','class','U102',True),
('ST01A -A','Agricultural machinery store','class','U103',True),
('ST02A -A','Builders yard','class','U103',True),
('ST01A -B','Building equipment store','class','U103',True),
('ST02A','Bulk material stores','class','U103',True),
('ST02A -B','Cleaning materials store','class','U103',True),
('ST01A -C','Engineering equipment store','class','U103',True),
('ST01A','Equipment stores','class','U103',True),
('ST03A -A','Furniture depository','class','U103',True),
('ST03A -B','General goods store','class','U103',True),
('ST01A -D','Industrial and office machinery store','class','U103',True),
('ST02','Material stores','class','U103',True),
('ST03A -C','Refrigerated store','class','U103',True),
('ST01A -E','Sports equipment store','class','U103',True),
('ST','Storage','class','U103',True),
('WH01B -A','Agricultural machinery dealers place','class','U104',True),
('WH01A -C','Builders merchant''s place','class','U104',True),
('WH01A','Bulk dealing places','class','U104',True),
('WH01A -A','Coal and oil dealer''s place','class','U104',True),
('WH01A -B','Corn, seed and agricultural supplies dealer''s place','class','U104',True),
('WH01','Dealing in industrial materials, machinery and livestock places','class','U104',True),
('WH02A','Food and drink wholesaling places','class','U104',True),
('WH02A -A','Grocery and provisions confectionery and drinks wholesaling','class','U104',True),
('WH01B -B','Hides, skin and leather dealer''s place','class','U104',True),
('WH01C-A','Horses and livestock dealer','class','U104',True),
('WH01C','Horses and livestock dealing places','class','U104',True),
('WH01B','Industrial materials and other machinery dealing places','class','U104',True),
('WH02B -A','Petroleum products wholesaling place','class','U104',True),
('WH01B -C','Scrap and waste dealer','class','U104',True),
('WH01B -D','Timber dealer''s place','class','U104',True),
('WH02','Wholesale distribution places','class','U104',True),
('UL02A','Unused buildings','class','U111',True),
('UL01B','Unused formerly developed land','class','U111',False),
('UL01B -A','Cleared site','class','U111',True),
('UL01B -C','Protected land (unused)','class','U111',True),
('UL02A -B','Vacant building','class','U111',True),
('UL02A -A','Abandoned building','class','U112',True),
('UL01B -B','Mineral excavation or pit (dry)','class','U112',True),
('UL02A -A','Ruined building','class','U112',True),
('UL01B -D','Spoilt land','class','U112',True),
('UL01B -E','Waste heap or tip','class','U112',True),
('DF01','Defence establishments','class','U121',True),
('DF01A','Defence training places','class','U121',True),
('DF01A -A','Live firing military training area','class','U121',True),
('UL01A -A','Beach or sand dune','class','U131',False),
('UL01C-A','Canal (unused)','class','U131',False),
('UL01A -B','Cliff or natural outcrop','class','U131',False),
('UL01C-B','Dock (unused)','class','U131',False),
('UL01A -C','Grass land','class','U131',False),
('UL01A -D','Heath and moorland','class','U131',False),
('UL01C-C','Mineral excavation or pit (wet)','class','U131',False),
('UL01A -E','Peat, bog, freshwater marsh and swamp','class','U131',False),
('UL01C-D','Pond or lake','class','U131',False),
('UL01A -F','Salt marsh (unused)','class','U131',False),
('UL01','Unused land and water','class','U131',False),
('UL01A','Unused land in natural or semi natural state','class','U131',False),
('UL01C','Unused water','class','U131',False),
('UL01C-E','Water course','class','U131',False),
('UL01A -G','Woodland and scrub','class','U131',False),
('AG08B -A','Coniferous forest','class','U022',False),
('AG08B -D','Deciduous forest','class','U022',False),
('AG08B','Forestry places','class','U022',False),
('AG08B -E','Mixed forest','class','U022',False),
('AG08B -B','Coppice','class','U021',False),
('AG08B -C','Coppice with standards','class','U021',False),
('AG08B -F','Tree nursery','class','U021',False),
('MI01D-A','Aggregate and stone handling installation','class','U031',True),
('MI01A -A','Chalk working','class','U031',False),
('MI01A -B','China clay working','class','U031',False),
('MI01C-A','China clay waste tip and settlement lagoon','class','U031',False),
('MI01A -C','Clay and shale working','class','U031',False),
('MI01A -D','Coal mine working','class','U031',False),
('MI01D-B','Coal handling installation','class','U031',True),
('MI01C-B','Coal waste tip and settlement lagoon','class','U031',False),
('MI01B -A','Colliery headgear','class','U031',True),
('MI01A -E','Gypsum/Anhydrite working','class','U031',False),
('MI01A -F','Igneous rock working','class','U031',False),
('MI01D-C','Iron ore handling installation','class','U031',True),
('MI01A -G','Limestone working','class','U031',True),
('MI01','Mineral extraction places','class','U031',False),
('MI01D-E','Mineral fertiliser handling installation','class','U031',True),
('MI01D-D','Non ferrous ore handling installation','class','U031',True),
('MI01D-F','Oil and gas handling installation','class','U031',True),
('MI01B -B','Oil and gas well head','class','U031',True),
('MI01B -C','Salt and brine pumping installation','class','U031',True),
('MI01A -H','Sand and gravel working','class','U031',False),
('MI01A -I','Sandstone working','class','U031',False),
('MI01A -K','Silica and moulding sand working','class','U031',False),
('MI01A -L','Slate working','class','U031',False),
('MI01C-C','Slate waste tip','class','U031',False),
('MI01B','Surface installations for underground mineral workings','class','U031',True),
('MI01A','Surface mineral workings','class','U031',False),
('MI01A -M','Vein mineral working','class','U031',False),
('MI01C-D','Vein mineral waste tip and settlement lagoon','class','U031',False),
('MI01C','Waste disposal areas from mineral working and processing','class','U031',False),
('LE01','Amenity, amusement and show places','class','#N/A',True),
('LE','Recreation and leisure places','class','#N/A',True),
('LE01B -A','Ancient monument','class','U041',True),
('LE01A -A','Botanical garden','class','U041',False),
('LE01A -B','Country park','class','U041',False),
('LE01A -C','Gardens','class','U041',False),
('LE01B -B','Monument','class','U041',True),
('LE01A','Outdoor amenity places','class','U041',False),
('LE01A -D','Park','class','U041',False),
('LE01A -E','Picnic site','class','U041',False),
('LE01A -F','Recreational open space','class','U041',False),
('LE01A -G','View point','class','U041',False),
('LE01A -A','Zoological garden','class','U041',False),
('LE01C','Amusement places','class','U042',True),
('LE01C-A','Aquarium','class','U042',True),
('LE01C-C','Bingo club','class','U042',True),
('LE01D-A','Broadcasting, filming and sound recording studio','class','U042',True),
('LE01C-D','Children''s playground','class','U042',False),
('LE01D-B','Cinema','class','U042',True),
('LE01D-C','Circus','class','U042',True),
('LE01D-D','Concert arena','class','U042',True),
('LE01D-E','Countryside interpretation centre','class','U042',True),
('LE01C-E','Dance hall','class','U042',True),
('LE01D-F','Display arena','class','U042',True),
('LE01C-F','Fun fair','class','U042',True),
('LE01C-G','Gaming club','class','U042',True),
('LE01C-H','Night club','class','U042',True),
('LE01D','Show places','class','U042',True),
('LE01D-G','Theatre','class','U042',True),
('LE02C-A','Art gallery','class','U043',True),
('LE02C','Galleries','class','U043',True),
('LE02A -A','Lending library','class','U043',True),
('LE02A','Libraries','class','U043',True),
('LE02','Libraries, museums and galleries','class','U043',True),
('LE02B -A','Museum','class','U043',True),
('LE02A -B','Reference','class','U043',True),
('LE03 I','Animal training and competing places','class','U044',True),
('LE03G-A','Archery range','class','U044',True),
('LE03A -A','Association football ground','class','U044',True),
('LE03E','Athletic game courses','class','U044',True),
('LE03D','Athletic games arenas','class','U044',True),
('LE03D-A','Athletic ground','class','U044',True),
('LE03B -A','Badminton court','class','U044',True),
('LE03C','Ball game courses','class','U044',True),
('LE03B','Ball game greens and courts','class','U044',True),
('LE03A','Ball game pitches and grounds','class','U044',True),
('LE03A -B','Baseball ground','class','U044',True),
('LE04B -A','Boating facilities','class','U044',True),
('LE03E -A','Bobsleigh course','class','U044',True),
('LE03B -B','Bowling green','class','U044',False),
('LE04B -B','Canoeing water','class','U044',False),
('LE03F -A','Caving place','class','U044',True),
('LE03F','Climbing, rambling and caving places','class','U044',False),
('LE03D-B','Combative sports place','class','U044',True),
('LE03A -C','Cricket ground','class','U044',False),
('LE03B -C','Croquet lawn','class','U044',False),
('LE03 I -A','Cross country horse trial course','class','U044',False),
('LE03E -B','Cross country running course','class','U044',False),
('LE03H-A','Cycling circuit','class','U044',True),
('LE03 I -B','Dog racing track','class','U044',True),
('LE03 I -C','Dog trials area','class','U044',True),
('LE03C-B','Golf course','class','U044',False),
('LE03C-A','Golf driving range','class','U044',False),
('LE03D-C','Gymnasium','class','U044',True),
('LE03A -D','Hockey ground','class','U044',True),
('LE03 I -D','Horse racing course','class','U044',True),
('LE03 I -E','Horse show jumping, dressage and trotting arena','class','U044',False),
('LE03 I -F','Horse training area','class','U044',True),
('LE03J -A','Hunting place','class','U044',False),
('LE03J','Hunting and shooting places','class','U044',True),
('LE03A -E','Hurling or shinty grounds','class','U044',True),
('LE03D-D','Ice rink','class','U044',True),
('LE03A -F','Lacrosse ground','class','U044',True),
('LE03H-B','Land sailing area','class','U044',False),
('LE03','Land sport places','class','U044',False),
('LE03H','Land vehicle performance places','class','U044',False),
('LE03B -D','Miniature golf course','class','U044',True),
('LE03H-C','Motor vehicle racing track','class','U044',False),
('LE03A -G','Polo ground','class','U044',True),
('LE04B -C','Power craft water','class','U044',False),
('LE03F -B','Rambling and fell walking','class','U044',False),
('LE03E -C','Road running and walking course','class','U044',False),
('LE03F -C','Rock climbing','class','U044',True),
('LE04C-A','Rod/recreational fishing place','class','U044',False),
('LE03D-E','Roller skating rink','class','U044',True),
('LE04B -D','Rowing water','class','U044',False),
('LE03A -H','Rugby football ground','class','U044',False),
('LE04B -E','Sailing','class','U044',True),
('LE03J -B','Shooting and stalking area','class','U044',False),
('LE03E -D','Skiing and tobogganing run','class','U044',False),
('LE03G-B','Small arms range','class','U044',False),
('LE03B -E','Squash court','class','U044',True),
('LE04A','Swimming and bathing','class','U044',True),
('LE04A -A','Swimming baths','class','U044',True),
('LE03G','Target shooting places','class','U044',True),
('LE03B -G','Ten pin bowling alley','class','U044',True),
('LE03B -F','Tennis court','class','U044',True),
('LE04C','Water recreation places','class','U044',True),
('LE04B -F','Water skiing place','class','U044',False),
('LE04','Water sport places','class','U044',False),
('LE04B','Watercraft places','class','U044',True),
('LE05A -A','Camping site','class','U045',False),
('LE05A -B','Holiday camp site','class','U045',False),
('LE05A','Holiday camps','class','U045',True),
('LE05A -C','Holiday caravan site','class','U045',False),
('LE05A -D','Youth hostel','class','U045',True),
('AG06B -A','Allotment gardens','class','U046',False),
('TR02C','Storage places for vehicles','class','U051',True),
('TR','Transport tracks and places','class','U051',True),
('TR01E -A','Access road','class','U051',False),
('TR01E -B','All purpose road','class','U051',False),
('TR01F -A','Branch line','class','U051',False),
('TR01C-A','Bridleway','class','U051',False),
('TR01D-A','Bus only way','class','U051',False),
('TR01D-B','Bus way','class','U051',False),
('TR01B -A','Cycle track','class','U051',False),
('TR01C-B','Drovers way','class','U051',False),
('TR01A -A','Footpath','class','U051',False),
('TR01','Land transport tracks','class','U051',False),
('TR01F -B','Light railway','class','U051',False),
('TR01E -C','Local distributor road','class','U051',False),
('TR01F -C','Main line','class','U051',False),
('TR01F -D','Mineral line','class','U051',False),
('TR01E -E','Motor vehicle practice circuit','class','U051',False),
('TR01E -D','Motor vehicle testing circuit','class','U051',False),
('TR01E -F','Motorway (special road)','class','U051',False),
('TR01C-C','Pony trekking route','class','U051',False),
('TR01A -B','Precinct','class','U051',False),
('TR01E -G','Primary distributor road','class','U051',False),
('TR01E -H','Processional route (road)','class','U051',False),
('TR01A -C','Processional route (walking or marching)','class','U051',False),
('TR01F','Railways','class','U051',False),
('TR01C-D','Ride','class','U051',False),
('TR01E','Roads','class','U051',False),
('TR01E -I','Secondary distributor road','class','U051',False),
('TR01F -E','Tramway','class','U051',False),
('TR01F -F','Underground line','class','U051',False),
('TR01A -D','Walkway','class','U051',False),
('TR02A -A','Aerial ropeway passenger terminal','class','U051',True),
('TR02A -B','Air passenger terminal','class','U051',True),
('TR02A -C','Airport','class','U051',True),
('TR02A -D','Bus station','class','U051',True),
('TR02A -E','Bus stop','class','U051',True),
('TR02A -F','Car park','class','U051',True),
('TR02A -G','Coach station','class','U051',True),
('TR02','Land transport places','class','U051',True),
('TR02A -H','Railway station','class','U051',True),
('TR02A -I','Ship passenger terminal','class','U051',True),
('TR02A','Terminals and interchanges for people','class','U051',True),
('TR02C-C','Car storage place','class','U053',True),
('TR02C-A','Aircraft hangar','class','U051',True),
('TR02C-B','Bus depot','class','U051',True),
('TR02C-D','Coach depot','class','U051',True),
('TR02C-E','Long stay lorry park','class','U051',True),
('TR02C-F','Railway sidings','class','U051',False),
('TR05A -A','Aerial ropeway','class','U055',False),
('TR02B -A','Air freight terminal','class','U055',True),
('TR02B -B','Container depot','class','U055',True),
('TR05A -B','Conveyor','class','U055',False),
('TR06A -A','Customs depot','class','U055',True),
('TR02B -C','Docks','class','U055',True),
('TR06','Goods handling places','class','U055',True),
('TR05A -C','Lift','class','U055',True),
('TR02B -D','Lorry transhipment park','class','U055',False),
('TR05A','Mechanical handling places','class','U055',True),
('TR02B -E','Railway goods siding','class','U055',False),
('TR02B -F','Railway goods yard','class','U055',False),
('TR02B -G','Railway sorting depot','class','U055',True),
('TR02B','Terminals and interchanges for goods','class','U055',True),
('TR04A -A','Anchorage','class','U056',False),
('TR04A -B','Boatyard','class','U056',False),
('TR03A -A','Canal','class','U056',False),
('TR04A -C','Marina','class','U056',False),
('TR04A -D','Mooring','class','U056',False),
('TR03A -B','River','class','U056',False),
('TR04A','Storage places for water craft','class','U056',True),
('TR03A','Water tracks','class','U056',False),
('TR04','Water transport places','class','U056',False),
('TR03','Water transport tracks','class','U056',False),
('UT','Utility services','class','#N/A',False),
('UT06A','District heating places','class','U061',True),
('UT06A -A','District heating plant','class','U061',True),
('UT02B -A','Electricity cableway','class','U061',True),
('UT02B','Electricity distribution places','class','U061',True),
('UT02A','Electricity production places','class','U061',True),
('UT02','Electricity supply places','class','U061',True),
('UT02B -B','Electricity transformer station','class','U061',True),
('UT01B','Gas distribution places','class','U061',True),
('UT01A -A','Gas holder','class','U061',True),
('UT01B -A','Gas pressure control station','class','U061',True),
('UT01A','Gas production and storage places','class','U061',True),
('UT01','Gas supply places','class','U061',True),
('UT01A -B','Gas works','class','U061',True),
('UT02A -A','Hydro electricity generating station','class','U061',True),
('TR05B -A','Oil pumping station','class','U061',True),
('TR05B -B','Pipeline','class','U061',False),
('UT02A -B','Thermal electricity generating station','class','U061',True),
('UT04A -A','Main drain','class','U062',False),
('UT03A -A','Reservoir','class','U062',False),
('UT04','Sewage disposal places','class','U062',True),
('UT04A','Sewage draining places','class','U062',True),
('UT04B -A','Sewage farm','class','U062',True),
('UT04A -B','Sewage pumping station','class','U062',True),
('UT04B','Sewage treatment places','class','U062',True),
('UT04B -B','Sewage treatment works','class','U062',True),
('UT03B','Water distribution places','class','U062',True),
('UT03C','Water extraction places','class','U062',True),
('UT03C-B','Water intake from rivers or streams','class','U062',True),
('UT03C-A','Water intake from springs','class','U062',True),
('UT03C-C','Water intake from underground sources','class','U062',True),
('UT03B -B','Water pipeline','class','U062',True),
('UT03B -A','Water pumping station','class','U062',True),
('UT03A','Water storage and treatment places','class','U062',True),
('UT03','Water supply places','class','U062',True),
('UT03A -B','Water tower','class','U062',True),
('UT03A -C','Water treatment works','class','U062',True),
('UT05A','Refuse disposal places','class','U063',False),
('UT05','Refuse disposal places','class','U063',False),
('UT05A -A','Refuse disposal plant','class','U063',False),
('UT05A -B','Refuse tip','class','U063',False),
('UT07B -A','Cemetery','class','U064',False),
('UT07A -A','Chapel of rest','class','U064',True),
('UT07B -B','Crematorium','class','U064',True),
('UT07B','Dead bodies disposal places','class','U064',True),
('UT07A','Dead bodies storage places','class','U064',True),
('UT07A -B','Mortuary','class','U064',True),
('UT08F','Direction finding places','class','U065',True),
('UT08F -B','Direction finding transmitter','class','U065',True),
('UT08F -A','Navigational light beacon','class','U065',False),
('UT08A','Postal service places','class','U065',True),
('UT08','Postal service, signalling and telecommunications places','class','U065',True),
('UT08A -A','Postal sorting depot','class','U065',True),
('UT08C-A','Radar beacon','class','U065',False),
('UT08C','Radar places','class','U065',True),
('UT08C-B','Radar station','class','U065',True),
('UT08D-B','Radio and television mast','class','U065',False),
('UT08D-A','Radio station','class','U065',True),
('UT08E -A','Satellite communication station','class','U065',True),
('UT08F -C','Signalling station','class','U065',True),
('UT08B -A','Telephone cableway','class','U065',False),
('UT08B -B','Telephone exchange','class','U065',True),
('UT08B -C','Telephone kiosk','class','U065',True),
('UT08D','Television and radio broadcasting places','class','U065',True),
('UT08D-C','Television station','class','U065',True),
('RS','Residences','class','#N/A',False),
('RS02A -A','Building converted to more than one dwelling','class','U071',True),
('RS02A -B','Bungalow','class','U071',True),
('RS02A -C','Detached house','class','U071',True),
('RS02A','Dwellings','class','U071',True),
('RS02A -D','Maisonette','class','U071',True),
('RS01C-A','Movable dwelling site','class','U071',False),
('RS02A -E','Non residential plus single dwelling','class','U071',True),
('RS02A -F','Purpose built block of flats','class','U071',True),
('RS01C-B','Residential caravan site','class','U071',False),
('RS02','Self contained residences','class','U071',True),
('RS02A -G','Semi detached house','class','U071',True),
('RS02A -H','Terraced house','class','U071',True),
('RS01A -A','Boarding house','class','U072',True),
('RS01','Group residences','class','U072',True),
('RS01A -B','Hotel','class','U072',True),
('RS01A -C','Residential club','class','U072',True),
('RS01A -D','Rooming house','class','U072',True),
('RS01B -A','Barracks','class','U073',True),
('CM04A -A','Children''s home','class','U073',True),
('RS01B','Communal homes','class','U073',True),
('CM04A -B','Handicapped and disabled people''s home','class','U073',True),
('CM04','Non medical care places','class','U073',True),
('CM04A','Non medical homes','class','U073',True),
('CM04A -C','Old people''s home','class','U073',True),
('RS01B -B','Residential retreat','class','U073',True),
('RS01B -C','School boarding house','class','U073',True),
('RS01B -D','Staff hostel','class','U073',True),
('CM','Community and health services','class','#N/A',False),
('CM01B -A','Ambulance station','class','U081',True),
('CM01A -A','Ante natal and post natal clinic','class','U081',True),
('CM01C-A','Artificial limb and appliance hospital','class','U081',True),
('CM01B','Auxiliary service centres medical','class','U081',True),
('CM01B -B','Blood transfusion centre','class','U081',True),
('CM01D-A','Convalescent home','class','U081',True),
('CM04B -A','Counselling agency','class','U081',True),
('CM04B','Counselling places','class','U081',True),
('CM01C-B','Dental hospital','class','U081',True),
('CM01A -B','Dentist''s surgery and consulting room','class','U081',True),
('CM01A -C','Dispensary','class','U081',True),
('CM01A -D','Doctor''s surgery and consulting room','class','U081',True),
('CM01C-C','Ear, nose and throat hospital','class','U081',True),
('CM01C-D','Eye hospital','class','U081',True),
('CM01A -E','Eye clinic and optician''s surgery and consulting room','class','U081',True),
('CM01B -C','Family planning clinic','class','U081',True),
('CM01A -F','Foot clinic and chiropodist''s surgery and consulting room','class','U081',True),
('CM01B -D','Forensic medicine centre','class','U081',True),
('CM01C-E','General hospital','class','U081',True),
('CM01C-F','Geriatric hospital','class','U081',True),
('CM01','Health care places','class','U081',True),
('CM01A -G','Health centre','class','U081',True),
('CM01A -H','Hearing aid centre','class','U081',True),
('CM01C','Hospitals','class','U081',True),
('CM01C-G','Isolation hospital','class','U081',True),
('CM01C-H','Maternity hospital','class','U081',True),
('CM01B','Medical auxiliary service centres','class','U081',True),
('CM01A','Medical diagnosis and treatment centres','class','U081',True),
('CM02A','Medical research establishments','class','U081',True),
('CM02A -A','Medical research laboratory','class','U081',True),
('CM01C-I','Mental hospital','class','U081',True),
('CM01A -I','Mental clinic','class','U081',True),
('CM01A -J','Nervous disorders clinic','class','U081',True),
('CM01A -K','Occupational therapy and physiotherapy clinic','class','U081',True),
('CM01C-J','Orthopaedic hosdpital','class','U081',True),
('CM01A -L','Orthopaedic and rheumatic clinic','class','U081',True),
('CM01B -E','Radiography centre','class','U081',True),
('CM01A -M','Surgeon''s surgery and consulting room','class','U081',True),
('CM07A','Places of worship','class','U082',True),
('ED01F -A','Adult education centre','class','U083',True),
('ED02A -A','Archaeological site','class','U083',False),
('ED01F -B','College of further education','class','U083',True),
('ED01F -C','College of technology','class','U083',True),
('ED01A -A','Day nursery school','class','U083',True),
('ED01','Education places','class','U083',True),
('ED01B -A','Infant school','class','U083',True),
('ED01B -B','Junior school','class','U083',True),
('ED01C-A','Middle school','class','U083',True),
('ED02B -A','Nature reserve','class','U083',False),
('ED02B','Nature reserves and sanctuaries','class','U083',False),
('ED01A -B','Nursery school','class','U083',True),
('ED02A -B','Observatory','class','U083',True),
('ED01F -D','Polytechnic','class','U083',True),
('ED01A','Pre primary schools','class','U083',True),
('ED01B','Primary schools','class','U083',True),
('ED02A','Research establishments','class','U083',True),
('ED02','Research places','class','U083',True),
('ED01D-A','Secondary school','class','U083',True),
('ED01D','Secondary schools','class','U083',True),
('ED02B -B','Site of special scientific interest','class','U083',False),
('ED01D-B','Sixth form college','class','U083',True),
('ED01E -A','Special school','class','U083',True),
('ED01F','Specialised, higher and further education centres','class','U083',True),
('ED01F -E','Teacher training college','class','U083',True),
('ED01F -F','Technical college','class','U083',True),
('ED01F -G','University teaching establishment','class','U083',True),
('CM06A -A','Advertising hoarding','class','U084',False),
('CM06A','Advertising places','class','U084',False),
('CM05A -A','Approved school','class','U084',True),
('CM08A -A','Arbitration court','class','U084',True),
('CM05A -B','Borstal institution','class','U084',True),
('CM06B -A','Church hall','class','U084',True),
('CM05B -A','Civil Defence centre','class','U084',True),
('CM06B -B','Club meeting place','class','U084',True),
('CM05B -B','Coastguard station','class','U084',True),
('CM06','Communication places','class','U084',True),
('CM06B -C','Community centre','class','U084',True),
('CM05','Community protection services','class','U084',True),
('CM08A','Courts','class','U084',True),
('CM05A','Detention places','class','U084',True),
('CM05B -C','Fire station','class','U084',True),
('CM08','Justice administration places','class','U084',True),
('CM08A -B','Law court','class','U084',True),
('CM05B -E','Life boat station','class','U084',True),
('CM07A -A','Place of worship','class','U084',True),
('CM05B -D','Police station','class','U084',True),
('CM05A -C','Prison','class','U084',True),
('CM05A -D','Prison rehabilitation centre','class','U084',True),
('CM05B','Protection places','class','U084',True),
('CM03A -A','Public bath','class','U084',True),
('CM03A -B','Public convenience','class','U084',True),
('CM05A -E','Remand centre','class','U084',True),
('CM05A -F','Remand classifying centre','class','U084',True),
('CM05A -G','Remand home','class','U084',True),
('CM03','Sanitation places','class','U084',True),
('CM03A','Sanitation places','class','U084',True),
('CM06B','Social meeting places','class','U084',True),
('CM08A -C','Tribunal place','class','U084',True),
('RT03','Catering service places','class','U093',True),
('RT01','Retail distribution places','class','U091',True),
('RT','Retail distribution and servicing places','class','U091',True),
('RT01A -A','Bakers shop','class','U091',True),
('RT01B -A','Beauty salon','class','U091',True),
('RT02B -A','Boot and shoe repair establishment','class','U091',True),
('RT01A -B','Butcher''s shop','class','U091',True),
('RT01D-A','Caravan sales place','class','U091',True),
('RT01F -A','Cash and carry store','class','U091',True),
('RT01A -C','Cats meat shop','class','U091',True),
('RT01B -C','Clothing and footwear shop','class','U091',True),
('RT01B -B','Confectionery, tobacco and newspaper shop','class','U091',True),
('RT01A -D','Dairy shop','class','U091',True),
('RT01F -B','Department store','class','U091',True),
('RT02B -B','Dry cleaning and clothing repair establishment','class','U091',True),
('RT01B -D','Duplicating and copying centre','class','U091',True),
('RT01C-A','Electricity showroom','class','U091',True),
('RT01A -E','Fish shop','class','U091',True),
('RT01A','Food and drink shops','class','U091',True),
('RT01A -F','Fried fish shop','class','U091',True),
('RT01A -G','Frozen food shop','class','U091',True),
('RT01C-B','Gas showroom','class','U091',True),
('RT01B -E','General stores','class','U091',True),
('RT01A -H','Green grocer''s shop','class','U091',True),
('RT01A -I','Grocery and provision','class','U091',True),
('RT01B -F','Hairdresser''s shop','class','U091',True),
('RT01A -J','Hot food shop','class','U091',True),
('RT01C-C','Household goods shop','class','U091',True),
('RT01C','Household goods shops and showrooms','class','U091',True),
('RT01F','Hybrid shops and stores','class','U091',True),
('RT01F -C','Hypermarket','class','U091',True),
('RT02B -C','Launderette','class','U091',True),
('RT02B -D','Laundry (cleaning only)','class','U091',True),
('RT01B -G','Laundry, cleaning and repairing shop (receiving)','class','U091',True),
('RT02','Maintenance and repair places','class','U091',True),
('RT01D-C','Motor vehicle dealer display area','class','U091',True),
('RT01D','Motor vehicle goods shops and filling stations','class','U091',True),
('RT02A','Motor vehicle maintenance and repair places','class','U091',True),
('RT02A -A','Motor vehicle repair garage','class','U091',True),
('RT01D-D','Motor vehicle sales','class','U091',True),
('RT01D-B','Motor vehicle spare parts and accessories','class','U091',True),
('RT02A -B','Motor vehicle testing station','class','U091',True),
('RT01A -K','Off licence','class','U091',True),
('RT02B','Personal and household goods repair and cleaning places','class','U091',True),
('RT01B -H','Pet animal and bird shop','class','U091',True),
('RT01D-E','Petrol and oil filling station','class','U091',True),
('RT01B -I','Photographic service shop','class','U091',True),
('RT01B -J','Post office','class','U091',True),
('RT01E -A','Retail market place','class','U091',True),
('RT01F -D','Supermarket','class','U091',True),
('RT01B -K','Ticket agency','class','U091',True),
('RT01B -L','Travel agency','class','U091',True),
('RT01A -L','Tripe shop','class','U091',True),
('RT01D-F','Tyre retailing and fitting place','class','U091',True),
('RT01B -M','Undertaker','class','U091',True),
('OF02A -A','Bank','class','U092',True),
('LE01C-B','Betting office','class','U092',True),
('OF02A -B','Building society office','class','U092',True),
('OF02A','Financial service offices','class','U092',True),
('OF02A -C','Insurance office','class','U092',True),
('RT03B','Catering places','class','U093',True),
('RT03B -A','Restaurant','class','U093',True),
('RT03A -A','Public house','class','U094',True);