Merge pull request #625 from colouring-london/feature/verification
Feature/verification
This commit is contained in:
commit
488a5da1bb
5536
app/package-lock.json
generated
5536
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,57 +13,58 @@
|
||||
"start:prod": "NODE_ENV=production node build/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||
"@mapbox/sphericalmercator": "^1.1.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap": "^4.5.0",
|
||||
"canvas-confetti": "^1.2.0",
|
||||
"connect-pg-simple": "^6.1.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"express-session": "^1.17.1",
|
||||
"leaflet": "^1.6.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mapnik": "^4.4.0",
|
||||
"node-fs": "^0.1.7",
|
||||
"nodemailer": "^6.4.6",
|
||||
"nodemailer": "^6.4.11",
|
||||
"pg-promise": "^8.7.5",
|
||||
"query-string": "^6.12.0",
|
||||
"query-string": "^6.13.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-leaflet": "^1.0.1",
|
||||
"react-leaflet-universal": "^1.2.0",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-leaflet-universal": "^2.2.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"serialize-javascript": "^2.1.1",
|
||||
"sharp": "^0.22.1",
|
||||
"use-throttle": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.5",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/jest": "^24.9.1",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/lodash": "^4.14.158",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/mapbox__sphericalmercator": "^1.1.3",
|
||||
"@types/node": "^12.12.35",
|
||||
"@types/node": "^12.12.53",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/react": "^16.9.33",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-leaflet": "^2.5.1",
|
||||
"@types/react": "^16.9.44",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-leaflet": "^2.5.2",
|
||||
"@types/react-router-dom": "^4.3.5",
|
||||
"@types/sharp": "^0.22.3",
|
||||
"@types/webpack-env": "^1.15.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||
"@typescript-eslint/parser": "^2.27.0",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||
"@typescript-eslint/parser": "^2.34.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-jest": "^22.21.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"razzle": "^3.1.3",
|
||||
"razzle-plugin-typescript": "^3.0.0",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
"razzle": "^3.1.6",
|
||||
"razzle-plugin-typescript": "^3.1.6",
|
||||
"ts-jest": "^24.3.0",
|
||||
"typescript": "^3.8.3"
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
|
@ -35,7 +35,7 @@ const getBuildingsByReference = asyncController(async (req: express.Request, res
|
||||
}
|
||||
});
|
||||
|
||||
// GET individual building, POST building updates
|
||||
// GET individual building
|
||||
const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||
|
||||
@ -48,6 +48,7 @@ const getBuildingById = asyncController(async (req: express.Request, res: expres
|
||||
}
|
||||
});
|
||||
|
||||
// POST building attribute updates
|
||||
const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
let user_id;
|
||||
|
||||
@ -105,7 +106,7 @@ const getBuildingUPRNsById = asyncController(async (req: express.Request, res: e
|
||||
}
|
||||
});
|
||||
|
||||
// GET/POST like building
|
||||
// GET whether the user likes a building
|
||||
const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ like: false }); // not logged in, so cannot have liked
|
||||
@ -123,6 +124,7 @@ const getBuildingLikeById = asyncController(async (req: express.Request, res: ex
|
||||
}
|
||||
});
|
||||
|
||||
// GET building edit history
|
||||
const getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||
|
||||
@ -135,6 +137,7 @@ const getBuildingEditHistoryById = asyncController(async (req: express.Request,
|
||||
}
|
||||
});
|
||||
|
||||
// POST update to like/unlike building
|
||||
const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
|
||||
res.send(updatedBuilding);
|
||||
});
|
||||
|
||||
// GET building attributes (and values) as verified by user
|
||||
const getUserVerifiedAttributes = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({error: "Not logged in"}); // not logged in, so send empty object as no attributes verified
|
||||
}
|
||||
|
||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||
|
||||
try {
|
||||
const verifiedAttributes = await buildingService.getUserVerifiedAttributes(buildingId, req.session.user_id);
|
||||
res.send(verifiedAttributes);
|
||||
} catch (error) {
|
||||
if(error instanceof UserError) {
|
||||
throw new ApiUserError(error.message, error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// POST update to verify building attribute
|
||||
const verifyBuildingAttributes = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
|
||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||
const verifiedAttributes = req.body;
|
||||
|
||||
try {
|
||||
const success = await buildingService.verifyBuildingAttributes(buildingId, req.session.user_id, verifiedAttributes);
|
||||
res.send(success);
|
||||
} catch (error) {
|
||||
if(error instanceof UserError) {
|
||||
throw new ApiUserError(error.message, error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// GET latest revision id of any building
|
||||
const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const revisionId = await buildingService.getLatestRevisionId();
|
||||
@ -176,6 +221,8 @@ export default {
|
||||
getBuildingUPRNsById,
|
||||
getBuildingLikeById,
|
||||
updateBuildingLikeById,
|
||||
getUserVerifiedAttributes,
|
||||
verifyBuildingAttributes,
|
||||
getBuildingEditHistoryById,
|
||||
getLatestRevisionId
|
||||
};
|
||||
|
103
app/src/api/dataAccess/verify.ts
Normal file
103
app/src/api/dataAccess/verify.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import db from '../../db';
|
||||
import { DatabaseError, InvalidOperationError } from '../errors/general';
|
||||
|
||||
|
||||
export async function getBuildingVerifiedAttributes(buildingId: number): Promise<any[]> {
|
||||
try {
|
||||
return await (db).manyOrNone(
|
||||
`SELECT
|
||||
attribute,
|
||||
verified_value
|
||||
FROM
|
||||
building_verification
|
||||
WHERE
|
||||
building_id = $1;
|
||||
`,
|
||||
[buildingId]
|
||||
);
|
||||
} catch(error) {
|
||||
throw new DatabaseError(error.detail);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildingUserVerifiedAttributes(buildingId: number, userId: string): Promise<any> {
|
||||
try {
|
||||
const verifications = await (db).manyOrNone(
|
||||
`SELECT
|
||||
attribute,
|
||||
verified_value,
|
||||
date_trunc('minute', verification_timestamp) as verification_timestamp
|
||||
FROM
|
||||
building_verification
|
||||
WHERE
|
||||
building_id = $1
|
||||
AND user_id = $2;
|
||||
`,
|
||||
[buildingId, userId]
|
||||
);
|
||||
return asVerified(verifications)
|
||||
} catch(error) {
|
||||
throw new DatabaseError(error.detail);
|
||||
}
|
||||
}
|
||||
|
||||
function asVerified(verifications){
|
||||
const user_verified = {};
|
||||
for (const item of verifications) {
|
||||
user_verified[item.attribute] = item.verified_value
|
||||
}
|
||||
return user_verified;
|
||||
}
|
||||
|
||||
export async function updateBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string, value: any): Promise<void> {
|
||||
console.log(typeof value, value)
|
||||
try {
|
||||
if (typeof value === 'string'){
|
||||
// cast strings to text explicitly - otherwise Postgres fails to cast to jsonb directly
|
||||
await (db).none(
|
||||
`INSERT INTO
|
||||
building_verification
|
||||
( building_id, user_id, attribute, verified_value )
|
||||
VALUES
|
||||
($1, $2, $3, to_jsonb($4::text));
|
||||
`,
|
||||
[buildingId, userId, attribute, value]
|
||||
);
|
||||
} else {
|
||||
await (db).none(
|
||||
`INSERT INTO
|
||||
building_verification
|
||||
( building_id, user_id, attribute, verified_value )
|
||||
VALUES
|
||||
($1, $2, $3, to_jsonb($4));
|
||||
`,
|
||||
[buildingId, userId, attribute, value]
|
||||
);
|
||||
}
|
||||
} catch(error) {
|
||||
console.error(error)
|
||||
if(error.detail?.includes('already exists')) {
|
||||
const msg = 'User already verified that attribute for this building'
|
||||
throw new InvalidOperationError(msg);
|
||||
} else {
|
||||
throw new DatabaseError(error.detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string) : Promise<null> {
|
||||
try {
|
||||
return await (db).none(
|
||||
`DELETE FROM
|
||||
building_verification
|
||||
WHERE
|
||||
building_id = $1
|
||||
AND user_id = $2
|
||||
AND attribute = $3;
|
||||
`,
|
||||
[buildingId, userId, attribute]
|
||||
);
|
||||
} catch(error) {
|
||||
throw new DatabaseError(error.detail);
|
||||
}
|
||||
}
|
@ -31,6 +31,11 @@ router.route('/:building_id/like.json')
|
||||
.get(buildingController.getBuildingLikeById)
|
||||
.post(buildingController.updateBuildingLikeById);
|
||||
|
||||
// POST verify building attribute
|
||||
router.route('/:building_id/verify.json')
|
||||
.get(buildingController.getUserVerifiedAttributes)
|
||||
.post(buildingController.verifyBuildingAttributes);
|
||||
|
||||
router.route('/:building_id/history.json')
|
||||
.get(buildingController.getBuildingEditHistoryById);
|
||||
|
||||
|
@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition';
|
||||
import { BoundingBox } from '../../tiles/types';
|
||||
import * as buildingDataAccess from '../dataAccess/building';
|
||||
import * as likeDataAccess from '../dataAccess/like';
|
||||
import { UserError } from '../errors/general';
|
||||
import * as verifyDataAccess from '../dataAccess/verify';
|
||||
import { UserError, DatabaseError } from '../errors/general';
|
||||
|
||||
import { processBuildingUpdate } from './domainLogic/processBuildingUpdate';
|
||||
|
||||
@ -110,6 +111,7 @@ async function getBuildingById(id: number) {
|
||||
const building = await getCurrentBuildingDataById(id);
|
||||
|
||||
building.edit_history = await getBuildingEditHistory(id);
|
||||
building.verified = await getBuildingVerifications(building);
|
||||
|
||||
return building;
|
||||
} catch(error) {
|
||||
@ -161,7 +163,7 @@ async function getBuildingUPRNsById(id: number) {
|
||||
async function saveBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
|
||||
return await updateBuildingData(buildingId, userId, async () => {
|
||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||
|
||||
|
||||
// remove read-only fields from consideration
|
||||
delete processedBuilding.building_id;
|
||||
delete processedBuilding.revision_id;
|
||||
@ -204,6 +206,66 @@ async function unlikeBuilding(buildingId: number, userId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyBuildingAttributes(buildingId: number, userId: string, patch: object) {
|
||||
// get current building attribute values for comparison
|
||||
const building = await getCurrentBuildingDataById(buildingId);
|
||||
// keep track of attributes and values verified
|
||||
const verified = {}
|
||||
|
||||
// loop through attribute => value pairs to mark as verified
|
||||
for (let [key, value] of Object.entries(patch)) {
|
||||
// check key in whitelist
|
||||
if(BUILDING_FIELD_WHITELIST.has(key)) {
|
||||
// check value against current from database - JSON.stringify as hack for "any" data type
|
||||
if (JSON.stringify(value) == JSON.stringify(building[key])) {
|
||||
try {
|
||||
await verifyDataAccess.updateBuildingUserVerifiedAttribute(buildingId, userId, key, building[key]);
|
||||
verified[key] = building[key];
|
||||
} catch (error) {
|
||||
// possible reasons:
|
||||
// - not a building
|
||||
// - not a user
|
||||
// - user already verified this attribute for this building
|
||||
throw new DatabaseError(error);
|
||||
}
|
||||
} else {
|
||||
if (value === null) {
|
||||
await verifyDataAccess.removeBuildingUserVerifiedAttribute(buildingId, userId, key);
|
||||
} else {
|
||||
// not verifying current value
|
||||
const msg = `Attribute "${key}" with value "${value}" did not match latest saved value "${building[key]}"`;
|
||||
throw new DatabaseError(msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not a valid attribute
|
||||
const msg = `Attribute ${key} not recognised.`;
|
||||
throw new DatabaseError(msg);
|
||||
}
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
async function getUserVerifiedAttributes(buildingId: number, userId: string) {
|
||||
return await verifyDataAccess.getBuildingUserVerifiedAttributes(buildingId, userId);
|
||||
}
|
||||
|
||||
async function getBuildingVerifications(building) {
|
||||
const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id);
|
||||
|
||||
const verified = {};
|
||||
for (const element of BUILDING_FIELD_WHITELIST) {
|
||||
verified[element] = 0;
|
||||
}
|
||||
|
||||
for (const item of verifications) {
|
||||
if (JSON.stringify(building[item.attribute]) == JSON.stringify(item.verified_value)) {
|
||||
verified[item.attribute] += 1
|
||||
}
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
// === Utility functions ===
|
||||
|
||||
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
||||
@ -285,10 +347,10 @@ async function expireBuildingTileCache(buildingId: number) {
|
||||
|
||||
const BUILDING_FIELD_WHITELIST = new Set([
|
||||
'ref_osm_id',
|
||||
// 'location_name',
|
||||
'location_name',
|
||||
'location_number',
|
||||
// 'location_street',
|
||||
// 'location_line_two',
|
||||
'location_street',
|
||||
'location_line_two',
|
||||
'location_town',
|
||||
'location_postcode',
|
||||
'location_latitude',
|
||||
@ -378,5 +440,7 @@ export {
|
||||
saveBuilding,
|
||||
likeBuilding,
|
||||
unlikeBuilding,
|
||||
getLatestRevisionId
|
||||
getLatestRevisionId,
|
||||
verifyBuildingAttributes,
|
||||
getUserVerifiedAttributes
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ async function createUser(user) {
|
||||
throw { error: err.message };
|
||||
} else throw err;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return await db.one(
|
||||
`INSERT
|
||||
@ -89,7 +89,7 @@ async function getUserById(id: string) {
|
||||
try {
|
||||
return await db.one(
|
||||
`SELECT
|
||||
username, email, registered, api_key
|
||||
username, email, date_trunc('minute', registered) as registered, api_key
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -16,6 +16,7 @@ hydrate(
|
||||
user={data.user}
|
||||
building={data.building}
|
||||
building_like={data.building_like}
|
||||
user_verified={data.user_verified}
|
||||
revisionId={data.latestRevisionId}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
|
@ -30,6 +30,7 @@ interface AppProps {
|
||||
user?: User;
|
||||
building?: Building;
|
||||
building_like?: boolean;
|
||||
user_verified?: object;
|
||||
revisionId: number;
|
||||
}
|
||||
|
||||
@ -122,6 +123,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
{...props}
|
||||
building={this.props.building}
|
||||
building_like={this.props.building_like}
|
||||
user_verified={this.props.user_verified}
|
||||
user={this.state.user}
|
||||
revisionId={this.props.revisionId}
|
||||
/>
|
||||
|
@ -24,6 +24,7 @@ interface BuildingViewProps {
|
||||
building_like?: boolean;
|
||||
user?: any;
|
||||
selectBuilding: (building: Building) => void;
|
||||
user_verified?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,17 +18,22 @@ const CheckboxDataEntry: React.FunctionComponent<CheckboxDataEntryProps> = (prop
|
||||
copy={props.copy}
|
||||
/>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox"
|
||||
id={props.slug}
|
||||
name={props.slug}
|
||||
checked={!!props.value}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
onChange={e => props.onChange(props.slug, e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={props.slug}
|
||||
className="form-check-label">
|
||||
{props.title}
|
||||
<input className="form-check-input" type="checkbox"
|
||||
id={props.slug}
|
||||
name={props.slug}
|
||||
checked={!!props.value}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
onChange={e => props.onChange(props.slug, e.target.checked)}
|
||||
/>
|
||||
{
|
||||
props.value?
|
||||
<span><strong>Yes</strong>/No</span>
|
||||
:
|
||||
<span>Yes/<strong>No</strong></span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
@ -34,7 +34,7 @@ const DataEntry: React.FC<DataEntryProps> = (props) => {
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
|
||||
|
||||
maxLength={props.maxLength}
|
||||
placeholder={props.placeholder}
|
||||
valueTransform={props.valueTransform}
|
||||
|
@ -33,7 +33,7 @@ const NumericDataEntry: React.FunctionComponent<NumericDataEntryProps> = (props)
|
||||
min={props.min}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
onChange={e =>
|
||||
onChange={e =>
|
||||
props.onChange(
|
||||
props.slug,
|
||||
e.target.value === '' ? null : parseFloat(e.target.value)
|
||||
|
17
app/src/frontend/building/data-components/verification.css
Normal file
17
app/src/frontend/building/data-components/verification.css
Normal file
@ -0,0 +1,17 @@
|
||||
.verification-container {
|
||||
font-size: 0.83333rem;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.verification-status {
|
||||
padding-right: 0.5em;
|
||||
cursor: default;
|
||||
}
|
||||
.verification-container .verification-status .fa-w-16 {
|
||||
width: 20px;
|
||||
}
|
||||
.verification-container .btn {
|
||||
margin: 0 0.5em;
|
||||
padding: 0.125em 0.75em;
|
||||
vertical-align: baseline;
|
||||
font-size: 0.83333rem;
|
||||
}
|
75
app/src/frontend/building/data-components/verification.tsx
Normal file
75
app/src/frontend/building/data-components/verification.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { VerifyIcon } from '../../components/icons';
|
||||
|
||||
import './verification.css';
|
||||
|
||||
interface VerificationProps {
|
||||
slug: string;
|
||||
onVerify: (slug: string, verify: boolean, x: number, y: number) => void;
|
||||
user_verified: boolean;
|
||||
user_verified_as: string;
|
||||
verified_count: number;
|
||||
allow_verify: boolean;
|
||||
}
|
||||
|
||||
|
||||
class Verification extends Component<VerificationProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
handleClick(verify) {
|
||||
return (e) => {
|
||||
e.preventDefault();
|
||||
const x = e.clientX / document.body.clientWidth;
|
||||
const y = e.clientY / document.body.clientHeight;
|
||||
this.props.onVerify(this.props.slug, verify, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
let user_verified_as = props.user_verified_as;
|
||||
if (typeof user_verified_as === 'boolean') {
|
||||
user_verified_as = user_verified_as? 'Yes': 'No';
|
||||
}
|
||||
return (
|
||||
<div className="verification-container">
|
||||
<span
|
||||
className="verification-status"
|
||||
title={`Verified by ${props.verified_count} ${(props.verified_count == 1)? "person": "people"}`}
|
||||
>
|
||||
<VerifyIcon />
|
||||
{props.verified_count || 0}
|
||||
</span>
|
||||
{
|
||||
props.user_verified?
|
||||
<Fragment>
|
||||
Verified as
|
||||
"<span>{user_verified_as}</span>"
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
title="Remove my verification"
|
||||
disabled={!props.allow_verify}
|
||||
onClick={this.handleClick(false)}>
|
||||
Remove
|
||||
</button>
|
||||
</Fragment>
|
||||
:
|
||||
<Fragment>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
title="Confirm that the current value is correct"
|
||||
disabled={!props.allow_verify}
|
||||
onClick={this.handleClick(true)}>
|
||||
Verify
|
||||
</button>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Verification;
|
@ -1,5 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import Verification from './verification';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import { CopyProps } from '../data-containers/category-view-props';
|
||||
|
||||
@ -12,6 +13,12 @@ interface YearDataEntryProps {
|
||||
copy?: CopyProps;
|
||||
mode?: 'view' | 'edit' | 'multi-edit';
|
||||
onChange?: (key: string, value: any) => void;
|
||||
|
||||
onVerify: (slug: string, verify: boolean, x: number, y: number) => void;
|
||||
user_verified: boolean;
|
||||
user_verified_as: string;
|
||||
verified_count: number;
|
||||
allow_verify: boolean;
|
||||
}
|
||||
|
||||
class YearDataEntry extends Component<YearDataEntryProps, any> {
|
||||
@ -45,6 +52,15 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
|
||||
max={currentYear}
|
||||
// "type": "year_estimator"
|
||||
/>
|
||||
<Verification
|
||||
allow_verify={props.allow_verify}
|
||||
slug="date_year"
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified}
|
||||
user_verified_as={props.user_verified_as}
|
||||
verified_count={props.verified_count}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.date_upper.title}
|
||||
slug="date_upper"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { NavLink, Redirect } from 'react-router-dom';
|
||||
import Confetti from 'canvas-confetti';
|
||||
|
||||
import { apiPost } from '../apiHelpers';
|
||||
import ErrorBox from '../components/error-box';
|
||||
@ -24,6 +25,7 @@ interface DataContainerProps {
|
||||
mode: 'view' | 'edit';
|
||||
building?: Building;
|
||||
building_like?: boolean;
|
||||
user_verified?: any;
|
||||
selectBuilding: (building: Building) => void;
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
this.handleReset = this.handleReset.bind(this);
|
||||
this.handleLike = this.handleLike.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleVerify = this.handleVerify.bind(this);
|
||||
|
||||
this.toggleCopying = this.toggleCopying.bind(this);
|
||||
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||
@ -167,7 +170,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
`/api/buildings/${this.props.building.building_id}/like.json`,
|
||||
{like: like}
|
||||
);
|
||||
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
@ -188,7 +191,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
`/api/buildings/${this.props.building.building_id}.json`,
|
||||
this.state.buildingEdits
|
||||
);
|
||||
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
@ -199,6 +202,38 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
}
|
||||
}
|
||||
|
||||
async handleVerify(slug: string, verify: boolean, x: number, y: number) {
|
||||
const verifyPatch = {};
|
||||
if (verify) {
|
||||
verifyPatch[slug] = this.props.building[slug];
|
||||
} else {
|
||||
verifyPatch[slug] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiPost(
|
||||
`/api/buildings/${this.props.building.building_id}/verify.json`,
|
||||
verifyPatch
|
||||
);
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error});
|
||||
} else {
|
||||
if (verify) {
|
||||
Confetti({
|
||||
angle: 60,
|
||||
disableForReducedMotion: true,
|
||||
origin: {x, y},
|
||||
zIndex: 2000
|
||||
});
|
||||
}
|
||||
this.props.selectBuilding(this.props.building);
|
||||
}
|
||||
} catch(err) {
|
||||
this.setState({error: err});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.mode === 'edit' && !this.props.user){
|
||||
return <Redirect to="/sign-up.html" />;
|
||||
@ -281,9 +316,12 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
building={undefined}
|
||||
building_like={undefined}
|
||||
mode={this.props.mode}
|
||||
edited={false}
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onLike={this.handleLike}
|
||||
onVerify={this.handleVerify}
|
||||
user_verified={[]}
|
||||
/>
|
||||
</Fragment> :
|
||||
this.props.building != undefined ?
|
||||
@ -327,9 +365,13 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
building={currentBuilding}
|
||||
building_like={this.props.building_like}
|
||||
mode={this.props.mode}
|
||||
edited={edited}
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onLike={this.handleLike}
|
||||
onVerify={this.handleVerify}
|
||||
user_verified={this.props.user_verified}
|
||||
user={this.props.user}
|
||||
/>
|
||||
</form> :
|
||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
||||
|
@ -5,6 +5,7 @@ import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import TextboxDataEntry from '../data-components/textbox-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import YearDataEntry from '../data-components/year-data-entry';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
@ -15,7 +16,7 @@ import { CategoryViewProps } from './category-view-props';
|
||||
*/
|
||||
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<YearDataEntry
|
||||
@ -25,6 +26,12 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
|
||||
allow_verify={props.user !== undefined && props.building.date_year !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("date_year")}
|
||||
user_verified_as={props.user_verified.date_year}
|
||||
verified_count={props.building.verified.date_year}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title={dataFields.facade_year.title}
|
||||
@ -38,6 +45,15 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
max={currentYear}
|
||||
tooltip={dataFields.facade_year.tooltip}
|
||||
/>
|
||||
<Verification
|
||||
slug="facade_year"
|
||||
allow_verify={props.user !== undefined && props.building.facade_year !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("facade_year")}
|
||||
user_verified_as={props.user_verified.facade_year}
|
||||
verified_count={props.building.verified.facade_year}
|
||||
/>
|
||||
|
||||
<SelectDataEntry
|
||||
title={dataFields.date_source.title}
|
||||
slug="date_source"
|
||||
|
@ -10,9 +10,13 @@ interface CategoryViewProps {
|
||||
building: any; // TODO: add Building type with all fields
|
||||
building_like: boolean;
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
edited: boolean;
|
||||
copy: CopyProps;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onLike: (like: boolean) => void;
|
||||
onVerify: (slug: string, verify: boolean, x: number, y: number) => void;
|
||||
user_verified: any;
|
||||
user?: any;
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -5,6 +5,7 @@ import { dataFields } from '../../data_fields';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import UPRNsDataEntry from '../data-components/uprns-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
@ -21,8 +22,16 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
tooltip={dataFields.location_name.tooltip}
|
||||
placeholder="Building name (if any)"
|
||||
disabled={true}
|
||||
/>
|
||||
<Verification
|
||||
slug="location_name"
|
||||
allow_verify={props.user !== undefined && props.building.location_name !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("location_name")}
|
||||
user_verified_as={props.user_verified.location_name}
|
||||
verified_count={props.building.verified.location_name}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.location_number.title}
|
||||
slug="location_number"
|
||||
@ -33,6 +42,15 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={1}
|
||||
min={1}
|
||||
/>
|
||||
<Verification
|
||||
slug="location_number"
|
||||
allow_verify={props.user !== undefined && props.building.location_number !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("location_number")}
|
||||
user_verified_as={props.user_verified.location_number}
|
||||
verified_count={props.building.verified.location_number}
|
||||
/>
|
||||
|
||||
<DataEntry
|
||||
title={dataFields.location_street.title}
|
||||
slug="location_street"
|
||||
@ -40,8 +58,16 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
<Verification
|
||||
slug="location_street"
|
||||
allow_verify={props.user !== undefined && props.building.location_street !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("location_street")}
|
||||
user_verified_as={props.user_verified.location_street}
|
||||
verified_count={props.building.verified.location_street}
|
||||
/>
|
||||
|
||||
<DataEntry
|
||||
title={dataFields.location_line_two.title}
|
||||
slug="location_line_two"
|
||||
|
@ -6,6 +6,7 @@ import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
@ -23,6 +24,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_portal_link"
|
||||
allow_verify={props.user !== undefined && props.building.planning_portal_link !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_portal_link")}
|
||||
user_verified_as={props.user_verified.planning_portal_link}
|
||||
verified_count={props.building.verified.planning_portal_link}
|
||||
/>
|
||||
|
||||
<DataEntryGroup name="Planning Status">
|
||||
<CheckboxDataEntry
|
||||
title="Is a planning application live for this site?"
|
||||
@ -52,7 +62,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<InfoBox msg="For historical planning applications see Planning Portal link" />
|
||||
{/*
|
||||
{/*
|
||||
Move to Demolition:
|
||||
|
||||
<CheckboxDataEntry
|
||||
@ -84,6 +94,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_conservation_area"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_conservation_area !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_conservation_area")}
|
||||
user_verified_as={props.user_verified.planning_in_conservation_area}
|
||||
verified_count={props.building.verified.planning_in_conservation_area}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_conservation_area_name.title}
|
||||
slug="planning_conservation_area_name"
|
||||
@ -92,6 +110,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_conservation_area_name"
|
||||
allow_verify={props.user !== undefined && props.building.planning_conservation_area_name !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_conservation_area_name")}
|
||||
user_verified_as={props.user_verified.planning_conservation_area_name}
|
||||
verified_count={props.building.verified.planning_conservation_area_name}
|
||||
/>
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_in_list.title}
|
||||
slug="planning_in_list"
|
||||
@ -149,6 +176,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_heritage_at_risk_id"
|
||||
allow_verify={props.user !== undefined && props.building.planning_heritage_at_risk_id !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_heritage_at_risk_id")}
|
||||
user_verified_as={props.user_verified.planning_heritage_at_risk_id}
|
||||
verified_count={props.building.verified.planning_heritage_at_risk_id}
|
||||
/>
|
||||
|
||||
<DataEntry
|
||||
title={dataFields.planning_world_list_id.title}
|
||||
slug="planning_world_list_id"
|
||||
@ -157,6 +193,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_world_list_id"
|
||||
allow_verify={props.user !== undefined && props.building.planning_world_list_id !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_world_list_id")}
|
||||
user_verified_as={props.user_verified.planning_world_list_id}
|
||||
verified_count={props.building.verified.planning_world_list_id}
|
||||
/>
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_in_glher.title}
|
||||
slug="planning_in_glher"
|
||||
@ -165,6 +210,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_glher"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_glher !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_glher")}
|
||||
user_verified_as={props.user_verified.planning_in_glher}
|
||||
verified_count={props.building.verified.planning_in_glher}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_glher_url.title}
|
||||
slug="planning_glher_url"
|
||||
@ -173,6 +226,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_glher_url"
|
||||
allow_verify={props.user !== undefined && props.building.planning_glher_url !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_glher_url")}
|
||||
user_verified_as={props.user_verified.planning_glher_url}
|
||||
verified_count={props.building.verified.planning_glher_url}
|
||||
/>
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_in_apa.title}
|
||||
slug="planning_in_apa"
|
||||
@ -181,6 +243,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_apa"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_apa !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_apa")}
|
||||
user_verified_as={props.user_verified.planning_in_apa}
|
||||
verified_count={props.building.verified.planning_in_apa}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_apa_name.title}
|
||||
slug="planning_apa_name"
|
||||
@ -189,6 +259,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_apa_name"
|
||||
allow_verify={props.user !== undefined && props.building.planning_apa_name !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_apa_name")}
|
||||
user_verified_as={props.user_verified.planning_apa_name}
|
||||
verified_count={props.building.verified.planning_apa_name}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_apa_tier.title}
|
||||
slug="planning_apa_tier"
|
||||
@ -197,6 +275,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_apa_tier"
|
||||
allow_verify={props.user !== undefined && props.building.planning_apa_tier !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_apa_tier")}
|
||||
user_verified_as={props.user_verified.planning_apa_tier}
|
||||
verified_count={props.building.verified.planning_apa_tier}
|
||||
/>
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_in_local_list.title}
|
||||
slug="planning_in_local_list"
|
||||
@ -205,6 +292,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_local_list"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_local_list !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_local_list")}
|
||||
user_verified_as={props.user_verified.planning_in_local_list}
|
||||
verified_count={props.building.verified.planning_in_local_list}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_local_list_url.title}
|
||||
slug="planning_local_list_url"
|
||||
@ -213,6 +308,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_local_list_url"
|
||||
allow_verify={props.user !== undefined && props.building.planning_local_list_url !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_local_list_url")}
|
||||
user_verified_as={props.user_verified.planning_local_list_url}
|
||||
verified_count={props.building.verified.planning_local_list_url}
|
||||
/>
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_in_historic_area_assessment.title}
|
||||
slug="planning_in_historic_area_assessment"
|
||||
@ -221,6 +325,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_historic_area_assessment"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_historic_area_assessment !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_historic_area_assessment")}
|
||||
user_verified_as={props.user_verified.planning_in_historic_area_assessment}
|
||||
verified_count={props.building.verified.planning_in_historic_area_assessment}
|
||||
/>
|
||||
<DataEntry
|
||||
title={dataFields.planning_historic_area_assessment_url.title}
|
||||
slug="planning_historic_area_assessment_url"
|
||||
@ -229,6 +341,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_historic_area_assessment_url"
|
||||
allow_verify={props.user !== undefined && props.building.planning_historic_area_assessment_url !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_historic_area_assessment_url")}
|
||||
user_verified_as={props.user_verified.planning_historic_area_assessment_url}
|
||||
verified_count={props.building.verified.planning_historic_area_assessment_url}
|
||||
/>
|
||||
|
||||
</DataEntryGroup>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { dataFields } from '../../data_fields';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
@ -14,7 +15,6 @@ import { CategoryViewProps } from './category-view-props';
|
||||
const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<DataEntryGroup name="Storeys">
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_storeys_core.title}
|
||||
slug="size_storeys_core"
|
||||
@ -26,6 +26,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_storeys_core"
|
||||
allow_verify={props.user !== undefined && props.building.size_storeys_core !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_storeys_core")}
|
||||
user_verified_as={props.user_verified.size_storeys_core}
|
||||
verified_count={props.building.verified.size_storeys_core}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_storeys_attic.title}
|
||||
slug="size_storeys_attic"
|
||||
@ -37,6 +46,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_storeys_attic"
|
||||
allow_verify={props.user !== undefined && props.building.size_storeys_attic !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_storeys_attic")}
|
||||
user_verified_as={props.user_verified.size_storeys_attic}
|
||||
verified_count={props.building.verified.size_storeys_attic}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_storeys_basement.title}
|
||||
slug="size_storeys_basement"
|
||||
@ -48,6 +66,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_storeys_basement"
|
||||
allow_verify={props.user !== undefined && props.building.size_storeys_basement !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_storeys_basement")}
|
||||
user_verified_as={props.user_verified.size_storeys_basement}
|
||||
verified_count={props.building.verified.size_storeys_basement}
|
||||
/>
|
||||
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Height" collapsed={false}>
|
||||
<NumericDataEntry
|
||||
@ -60,6 +87,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_height_apex"
|
||||
allow_verify={props.user !== undefined && props.building.size_height_apex !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_height_apex")}
|
||||
user_verified_as={props.user_verified.size_height_apex}
|
||||
verified_count={props.building.verified.size_height_apex}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_height_eaves.title}
|
||||
slug="size_height_eaves"
|
||||
@ -83,6 +119,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_floor_area_ground"
|
||||
allow_verify={props.user !== undefined && props.building.size_floor_area_ground !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_floor_area_ground")}
|
||||
user_verified_as={props.user_verified.size_floor_area_ground}
|
||||
verified_count={props.building.verified.size_floor_area_ground}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_floor_area_total.title}
|
||||
slug="size_floor_area_total"
|
||||
@ -93,6 +138,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_floor_area_total"
|
||||
allow_verify={props.user !== undefined && props.building.size_floor_area_total !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_floor_area_total")}
|
||||
user_verified_as={props.user_verified.size_floor_area_total}
|
||||
verified_count={props.building.verified.size_floor_area_total}
|
||||
/>
|
||||
|
||||
</DataEntryGroup>
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_width_frontage.title}
|
||||
@ -104,6 +158,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
min={0}
|
||||
/>
|
||||
<Verification
|
||||
slug="size_width_frontage"
|
||||
allow_verify={props.user !== undefined && props.building.size_width_frontage !== null}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("size_width_frontage")}
|
||||
user_verified_as={props.user_verified.size_width_frontage}
|
||||
verified_count={props.building.verified.size_width_frontage}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_plot_area_total.title}
|
||||
slug="size_plot_area_total"
|
||||
|
@ -3,6 +3,7 @@ import React, { Fragment } from 'react';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
@ -32,6 +33,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="sust_breeam_rating"
|
||||
allow_verify={props.user !== undefined && props.building.sust_breeam_rating !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("sust_breeam_rating")}
|
||||
user_verified_as={props.user_verified.sust_breeam_rating}
|
||||
verified_count={props.building.verified.sust_breeam_rating}
|
||||
/>
|
||||
|
||||
<SelectDataEntry
|
||||
title={dataFields.sust_dec.title}
|
||||
slug="sust_dec"
|
||||
@ -42,6 +52,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="sust_dec"
|
||||
allow_verify={props.user !== undefined && props.building.sust_dec !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("sust_dec")}
|
||||
user_verified_as={props.user_verified.sust_dec}
|
||||
verified_count={props.building.verified.sust_dec}
|
||||
/>
|
||||
|
||||
<SelectDataEntry
|
||||
title={dataFields.sust_aggregate_estimate_epc.title}
|
||||
slug="sust_aggregate_estimate_epc"
|
||||
@ -53,6 +72,7 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.sust_retrofit_date.title}
|
||||
slug="sust_retrofit_date"
|
||||
@ -65,6 +85,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="sust_retrofit_date"
|
||||
allow_verify={props.user !== undefined && props.building.sust_retrofit_date !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("sust_retrofit_date")}
|
||||
user_verified_as={props.user_verified.sust_retrofit_date}
|
||||
verified_count={props.building.verified.sust_retrofit_date}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.sust_life_expectancy.title}
|
||||
slug="sust_life_expectancy"
|
||||
|
@ -4,6 +4,7 @@ import { dataFields } from '../../data_fields';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
@ -31,6 +32,15 @@ const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<Verification
|
||||
slug="building_attachment_form"
|
||||
allow_verify={props.user !== undefined && props.building.building_attachment_form !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("building_attachment_form")}
|
||||
user_verified_as={props.user_verified.building_attachment_form}
|
||||
verified_count={props.building.verified.building_attachment_form}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.date_change_building_use.title}
|
||||
slug="date_change_building_use"
|
||||
|
@ -7,6 +7,7 @@ import MultiDataEntry from '../data-components/multi-data-entry/multi-data-entry
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
import Verification from '../data-components/verification';
|
||||
|
||||
/**
|
||||
* Use view/edit section
|
||||
@ -29,6 +30,14 @@ const UseView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
showAllOptionsOnEmpty={true}
|
||||
addOnAutofillSelect={true}
|
||||
/>
|
||||
<Verification
|
||||
slug="current_landuse_group"
|
||||
allow_verify={props.user !== undefined && props.building.current_landuse_group !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("current_landuse_group")}
|
||||
user_verified_as={props.user_verified.current_landuse_group && props.user_verified.current_landuse_group.join(", ")}
|
||||
verified_count={props.building.verified.current_landuse_group}
|
||||
/>
|
||||
{
|
||||
props.mode != 'view' &&
|
||||
<InfoBox msg="Land use order, shown below, is automatically derived from the land use groups"></InfoBox>
|
||||
|
@ -2,8 +2,22 @@
|
||||
* Mini-library of icons
|
||||
*/
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faAngleLeft, faAngleRight, faCaretDown, faCaretRight, faCaretUp, faCheck, faCheckDouble,
|
||||
faEye, faInfoCircle, faPaintBrush, faQuestionCircle, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faCaretDown,
|
||||
faCaretRight,
|
||||
faCaretUp,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckDouble,
|
||||
faEye,
|
||||
faInfoCircle,
|
||||
faPaintBrush,
|
||||
faQuestionCircle,
|
||||
faSearch,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
|
||||
@ -13,6 +27,7 @@ library.add(
|
||||
faPaintBrush,
|
||||
faTimes,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckDouble,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
@ -51,6 +66,10 @@ const SaveDoneIcon = () => (
|
||||
<FontAwesomeIcon icon="check-double" />
|
||||
);
|
||||
|
||||
const VerifyIcon = () => (
|
||||
<FontAwesomeIcon icon="check-circle" />
|
||||
)
|
||||
|
||||
const BackIcon = () => (
|
||||
<FontAwesomeIcon icon="angle-left" />
|
||||
);
|
||||
@ -88,5 +107,6 @@ export {
|
||||
DownIcon,
|
||||
UpIcon,
|
||||
RightIcon,
|
||||
SearchIcon
|
||||
SearchIcon,
|
||||
VerifyIcon
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||
building_like?: boolean;
|
||||
user?: any;
|
||||
revisionId?: number;
|
||||
user_verified?: object;
|
||||
}
|
||||
|
||||
interface MapAppState {
|
||||
@ -33,6 +34,7 @@ interface MapAppState {
|
||||
revision_id: number;
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
user_verified: object;
|
||||
}
|
||||
|
||||
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
@ -43,7 +45,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
category: this.getCategory(props.match.params.category),
|
||||
revision_id: props.revisionId || 0,
|
||||
building: props.building,
|
||||
building_like: props.building_like
|
||||
building_like: props.building_like,
|
||||
user_verified: props.user_verified || {}
|
||||
};
|
||||
|
||||
this.selectBuilding = this.selectBuilding.bind(this);
|
||||
@ -60,7 +63,10 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchLatestRevision();
|
||||
this.fetchBuildingData();
|
||||
|
||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
||||
this.fetchBuildingData(strictParseInt(this.props.match.params.building));
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLatestRevision() {
|
||||
@ -77,27 +83,29 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
* Fetches building data if a building is selected but no data provided through
|
||||
* props (from server-side rendering)
|
||||
*/
|
||||
async fetchBuildingData() {
|
||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
||||
try {
|
||||
// TODO: simplify API calls, create helpers for fetching data
|
||||
const buildingId = strictParseInt(this.props.match.params.building);
|
||||
let [building, building_uprns, building_like] = await Promise.all([
|
||||
apiGet(`/api/buildings/${buildingId}.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/like.json`)
|
||||
]);
|
||||
async fetchBuildingData(buildingId: number) {
|
||||
try {
|
||||
// TODO: simplify API calls, create helpers for fetching data
|
||||
let [building, building_uprns, building_like, user_verified] = await Promise.all([
|
||||
apiGet(`/api/buildings/${buildingId}.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/like.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/verify.json`)
|
||||
]);
|
||||
|
||||
building.uprns = building_uprns.uprns;
|
||||
building.uprns = building_uprns.uprns;
|
||||
|
||||
this.setState({
|
||||
building: building,
|
||||
building_like: building_like.like
|
||||
});
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
// TODO: add UI for API errors
|
||||
}
|
||||
this.setState({
|
||||
building: building,
|
||||
building_like: building_like.like,
|
||||
user_verified: user_verified
|
||||
});
|
||||
|
||||
this.increaseRevision(building.revision_id);
|
||||
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
// TODO: add UI for API errors
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,34 +140,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.increaseRevision(building.revision_id);
|
||||
// get UPRNs and update
|
||||
apiGet(`/api/buildings/${building.building_id}/uprns.json`)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
console.error(res);
|
||||
} else {
|
||||
building.uprns = res.uprns;
|
||||
this.setState({ building: building });
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.setState({ building: building });
|
||||
});
|
||||
this.fetchBuildingData(building.building_id);
|
||||
|
||||
// get if liked and update
|
||||
apiGet(`/api/buildings/${building.building_id}/like.json`)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
console.error(res);
|
||||
} else {
|
||||
this.setState({ building_like: res.like });
|
||||
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.setState({ building_like: false });
|
||||
});
|
||||
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,6 +231,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
cat={category}
|
||||
building={this.state.building}
|
||||
building_like={this.state.building_like}
|
||||
user_verified={this.state.user_verified}
|
||||
selectBuilding={this.selectBuilding}
|
||||
user={this.props.user}
|
||||
/>
|
||||
|
@ -76,6 +76,10 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-legend li {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-legend .key {
|
||||
display: inline-block;
|
||||
width: 1.3rem;
|
||||
|
@ -116,10 +116,10 @@ const LEGEND_CONFIG = {
|
||||
disclaimer: 'All data relating to designated buildings should be checked on the National Heritage List for England or local authority websites where used for planning or development purposes',
|
||||
elements: [
|
||||
{ color: '#95beba', text: 'In conservation area'},
|
||||
{ color: '#c72e08', text: 'Grade I listed'},
|
||||
{ color: '#e75b42', text: 'Grade II* listed'},
|
||||
{ color: '#c72e08', text: 'Grade I listed'},
|
||||
{ color: '#e75b42', text: 'Grade II* listed'},
|
||||
{ color: '#ffbea1', text: 'Grade II listed'},
|
||||
{ color: '#858ed4', text: 'Locally listed'},
|
||||
{ color: '#858ed4', text: 'Locally listed'},
|
||||
]
|
||||
},
|
||||
dynamics: {
|
||||
@ -228,14 +228,8 @@ class Legend extends React.Component<LegendProps, LegendState> {
|
||||
return (
|
||||
|
||||
<li key={item.color} >
|
||||
<tr>
|
||||
<td>
|
||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
||||
</td>
|
||||
<td>
|
||||
{ item.text }
|
||||
</td>
|
||||
</tr>
|
||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
||||
{ item.text }
|
||||
</li>
|
||||
|
||||
);
|
||||
|
@ -8,18 +8,20 @@ import {
|
||||
getBuildingById,
|
||||
getBuildingLikeById,
|
||||
getBuildingUPRNsById,
|
||||
getLatestRevisionId
|
||||
getLatestRevisionId,
|
||||
getUserVerifiedAttributes
|
||||
} from './api/services/building';
|
||||
import { getUserById } from './api/services/user';
|
||||
import App from './frontend/app';
|
||||
import { parseBuildingURL } from './parse';
|
||||
import asyncController from './api/routes/asyncController';
|
||||
|
||||
|
||||
// reference packed assets
|
||||
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
||||
|
||||
|
||||
function frontendRoute(req: express.Request, res: express.Response) {
|
||||
const frontendRoute = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const context: any = {}; // TODO: remove any
|
||||
const data: any = {}; // TODO: remove any
|
||||
context.status = 200;
|
||||
@ -31,34 +33,39 @@ function frontendRoute(req: express.Request, res: express.Response) {
|
||||
context.status = 404;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
userId ? getUserById(userId) : undefined,
|
||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
||||
getLatestRevisionId()
|
||||
]).then(function ([user, building, uprns, buildingLike, latestRevisionId]) {
|
||||
try {
|
||||
let [user, building, uprns, buildingLike, userVerified, latestRevisionId] = await Promise.all([
|
||||
userId ? getUserById(userId) : undefined,
|
||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
||||
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||
getLatestRevisionId()
|
||||
]);
|
||||
|
||||
if (isBuilding && typeof (building) === 'undefined') {
|
||||
context.status = 404;
|
||||
}
|
||||
data.user = user;
|
||||
data.building = building;
|
||||
data.building_like = buildingLike;
|
||||
data.user_verified = userVerified;
|
||||
if (data.building != null) {
|
||||
data.building.uprns = uprns;
|
||||
}
|
||||
data.latestRevisionId = latestRevisionId;
|
||||
renderHTML(context, data, req, res);
|
||||
}).catch(error => {
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
data.user = undefined;
|
||||
data.building = undefined;
|
||||
data.building_like = undefined;
|
||||
data.user_verified = {}
|
||||
data.latestRevisionId = 0;
|
||||
context.status = 500;
|
||||
renderHTML(context, data, req, res);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderHTML(context, data, req, res) {
|
||||
const markup = renderToString(
|
||||
@ -93,7 +100,7 @@ function renderHTML(context, data, req, res) {
|
||||
<meta property="og:image" content="https://colouring.london/images/logo-cl-square.png" />
|
||||
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Colouring London">
|
||||
|
2
migrations/018.verification.down.sql
Normal file
2
migrations/018.verification.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Building verification
|
||||
DROP TABLE IF EXISTS building_verification;
|
28
migrations/018.verification.up.sql
Normal file
28
migrations/018.verification.up.sql
Normal file
@ -0,0 +1,28 @@
|
||||
-- Building verification
|
||||
|
||||
-- Users can verify the correctness of individual building attribute values.
|
||||
|
||||
-- For a building, it's most useful to know the count of verifications of each
|
||||
-- attribute with current (or past) values.
|
||||
|
||||
-- For a user, it's useful to show which attributes they have already verified
|
||||
-- on a given building.
|
||||
|
||||
|
||||
-- Store user-building-attribute verification
|
||||
CREATE TABLE IF NOT EXISTS building_verification (
|
||||
verification_id serial PRIMARY KEY,
|
||||
verification_timestamp TIMESTAMP default NOW(),
|
||||
building_id integer REFERENCES buildings,
|
||||
user_id uuid REFERENCES users,
|
||||
attribute varchar, -- bit of a hack to refer to any `buildings` table column name
|
||||
verified_value jsonb -- bit of a hack to include "any" value
|
||||
);
|
||||
CREATE INDEX building_verification_idx ON building_verification ( building_id );
|
||||
CREATE INDEX user_verification_idx ON building_verification ( user_id );
|
||||
CREATE INDEX building_user_verification_idx ON building_verification ( building_id, user_id );
|
||||
|
||||
-- Enforce that a user only has one opinion about the correct value of an
|
||||
-- attribute for a given building (don't need to allow multiple verified_values)
|
||||
ALTER TABLE building_verification ADD CONSTRAINT verify_building_attribute_once
|
||||
UNIQUE ( building_id, user_id, attribute );
|
@ -47,6 +47,8 @@ GRANT USAGE ON ALL SEQUENCES IN SCHEMA public to appusername;
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO appusername;
|
||||
-- read map search locations
|
||||
GRANT SELECT ON TABLE search_locations to appusername;
|
||||
-- add/save user building attribute verification
|
||||
GRANT SELECT, INSERT, DELETE ON TABLE building_verification TO appusername;
|
||||
```
|
||||
|
||||
Set or update passwords:
|
||||
@ -57,8 +59,8 @@ psql -c "ALTER USER appusername WITH PASSWORD 'longsecurerandompassword';"
|
||||
|
||||
## File naming syntax
|
||||
|
||||
Initial up and down migrations as `###.name.up.sql` file number should be sequential
|
||||
and incremental to last migrations file number is same for up/down.
|
||||
Initial up and down migrations as `###.name.up.sql` file number should be sequential
|
||||
and incremental to last migrations file number is same for up/down.
|
||||
|
||||
If adjusting a prior migration syntax is:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user