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"
|
"start:prod": "NODE_ENV=production node build/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||||
"@mapbox/sphericalmercator": "^1.1.0",
|
"@mapbox/sphericalmercator": "^1.1.0",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"body-parser": "^1.19.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",
|
"connect-pg-simple": "^6.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.0",
|
"express-session": "^1.17.1",
|
||||||
"leaflet": "^1.6.0",
|
"leaflet": "^1.6.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mapnik": "^4.4.0",
|
"mapnik": "^4.4.0",
|
||||||
"node-fs": "^0.1.7",
|
"node-fs": "^0.1.7",
|
||||||
"nodemailer": "^6.4.6",
|
"nodemailer": "^6.4.11",
|
||||||
"pg-promise": "^8.7.5",
|
"pg-promise": "^8.7.5",
|
||||||
"query-string": "^6.12.0",
|
"query-string": "^6.13.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-leaflet": "^1.0.1",
|
"react-leaflet": "^1.0.1",
|
||||||
"react-leaflet-universal": "^1.2.0",
|
"react-leaflet-universal": "^2.2.1",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^5.2.0",
|
||||||
"serialize-javascript": "^2.1.1",
|
"serialize-javascript": "^2.1.1",
|
||||||
"sharp": "^0.22.1",
|
"sharp": "^0.22.1",
|
||||||
"use-throttle": "0.0.3"
|
"use-throttle": "0.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.5",
|
"@types/express": "^4.17.7",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
"@types/jest": "^24.9.1",
|
"@types/jest": "^24.9.1",
|
||||||
"@types/lodash": "^4.14.149",
|
"@types/lodash": "^4.14.158",
|
||||||
"@types/lodash.isequal": "^4.5.5",
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
"@types/mapbox__sphericalmercator": "^1.1.3",
|
"@types/mapbox__sphericalmercator": "^1.1.3",
|
||||||
"@types/node": "^12.12.35",
|
"@types/node": "^12.12.53",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/react": "^16.9.33",
|
"@types/react": "^16.9.44",
|
||||||
"@types/react-dom": "^16.9.6",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-leaflet": "^2.5.1",
|
"@types/react-leaflet": "^2.5.2",
|
||||||
"@types/react-router-dom": "^4.3.5",
|
"@types/react-router-dom": "^4.3.5",
|
||||||
"@types/sharp": "^0.22.3",
|
"@types/sharp": "^0.22.3",
|
||||||
"@types/webpack-env": "^1.15.1",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||||
"@typescript-eslint/parser": "^2.27.0",
|
"@typescript-eslint/parser": "^2.34.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-jest": "^22.21.0",
|
"eslint-plugin-jest": "^22.21.0",
|
||||||
"eslint-plugin-react": "^7.19.0",
|
"eslint-plugin-react": "^7.20.5",
|
||||||
"razzle": "^3.1.3",
|
"razzle": "^3.1.6",
|
||||||
"razzle-plugin-typescript": "^3.0.0",
|
"razzle-plugin-typescript": "^3.1.6",
|
||||||
"ts-jest": "^24.3.0",
|
"ts-jest": "^24.3.0",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.9.7"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transform": {
|
"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 getBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
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) => {
|
const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
let user_id;
|
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) => {
|
const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if (!req.session.user_id) {
|
if (!req.session.user_id) {
|
||||||
return res.send({ like: false }); // not logged in, so cannot have liked
|
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 getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
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) => {
|
const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if (!req.session.user_id) {
|
if (!req.session.user_id) {
|
||||||
return res.send({ error: 'Must be logged in' });
|
return res.send({ error: 'Must be logged in' });
|
||||||
@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
|
|||||||
res.send(updatedBuilding);
|
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) => {
|
const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const revisionId = await buildingService.getLatestRevisionId();
|
const revisionId = await buildingService.getLatestRevisionId();
|
||||||
@ -176,6 +221,8 @@ export default {
|
|||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
getBuildingLikeById,
|
getBuildingLikeById,
|
||||||
updateBuildingLikeById,
|
updateBuildingLikeById,
|
||||||
|
getUserVerifiedAttributes,
|
||||||
|
verifyBuildingAttributes,
|
||||||
getBuildingEditHistoryById,
|
getBuildingEditHistoryById,
|
||||||
getLatestRevisionId
|
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)
|
.get(buildingController.getBuildingLikeById)
|
||||||
.post(buildingController.updateBuildingLikeById);
|
.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')
|
router.route('/:building_id/history.json')
|
||||||
.get(buildingController.getBuildingEditHistoryById);
|
.get(buildingController.getBuildingEditHistoryById);
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition';
|
|||||||
import { BoundingBox } from '../../tiles/types';
|
import { BoundingBox } from '../../tiles/types';
|
||||||
import * as buildingDataAccess from '../dataAccess/building';
|
import * as buildingDataAccess from '../dataAccess/building';
|
||||||
import * as likeDataAccess from '../dataAccess/like';
|
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';
|
import { processBuildingUpdate } from './domainLogic/processBuildingUpdate';
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ async function getBuildingById(id: number) {
|
|||||||
const building = await getCurrentBuildingDataById(id);
|
const building = await getCurrentBuildingDataById(id);
|
||||||
|
|
||||||
building.edit_history = await getBuildingEditHistory(id);
|
building.edit_history = await getBuildingEditHistory(id);
|
||||||
|
building.verified = await getBuildingVerifications(building);
|
||||||
|
|
||||||
return building;
|
return building;
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
@ -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 ===
|
// === Utility functions ===
|
||||||
|
|
||||||
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
||||||
@ -285,10 +347,10 @@ async function expireBuildingTileCache(buildingId: number) {
|
|||||||
|
|
||||||
const BUILDING_FIELD_WHITELIST = new Set([
|
const BUILDING_FIELD_WHITELIST = new Set([
|
||||||
'ref_osm_id',
|
'ref_osm_id',
|
||||||
// 'location_name',
|
'location_name',
|
||||||
'location_number',
|
'location_number',
|
||||||
// 'location_street',
|
'location_street',
|
||||||
// 'location_line_two',
|
'location_line_two',
|
||||||
'location_town',
|
'location_town',
|
||||||
'location_postcode',
|
'location_postcode',
|
||||||
'location_latitude',
|
'location_latitude',
|
||||||
@ -378,5 +440,7 @@ export {
|
|||||||
saveBuilding,
|
saveBuilding,
|
||||||
likeBuilding,
|
likeBuilding,
|
||||||
unlikeBuilding,
|
unlikeBuilding,
|
||||||
getLatestRevisionId
|
getLatestRevisionId,
|
||||||
|
verifyBuildingAttributes,
|
||||||
|
getUserVerifiedAttributes
|
||||||
};
|
};
|
||||||
|
@ -89,7 +89,7 @@ async function getUserById(id: string) {
|
|||||||
try {
|
try {
|
||||||
return await db.one(
|
return await db.one(
|
||||||
`SELECT
|
`SELECT
|
||||||
username, email, registered, api_key
|
username, email, date_trunc('minute', registered) as registered, api_key
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -16,6 +16,7 @@ hydrate(
|
|||||||
user={data.user}
|
user={data.user}
|
||||||
building={data.building}
|
building={data.building}
|
||||||
building_like={data.building_like}
|
building_like={data.building_like}
|
||||||
|
user_verified={data.user_verified}
|
||||||
revisionId={data.latestRevisionId}
|
revisionId={data.latestRevisionId}
|
||||||
/>
|
/>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
|
@ -30,6 +30,7 @@ interface AppProps {
|
|||||||
user?: User;
|
user?: User;
|
||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
|
user_verified?: object;
|
||||||
revisionId: number;
|
revisionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{...props}
|
{...props}
|
||||||
building={this.props.building}
|
building={this.props.building}
|
||||||
building_like={this.props.building_like}
|
building_like={this.props.building_like}
|
||||||
|
user_verified={this.props.user_verified}
|
||||||
user={this.state.user}
|
user={this.state.user}
|
||||||
revisionId={this.props.revisionId}
|
revisionId={this.props.revisionId}
|
||||||
/>
|
/>
|
||||||
|
@ -24,6 +24,7 @@ interface BuildingViewProps {
|
|||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user?: any;
|
user?: any;
|
||||||
selectBuilding: (building: Building) => void;
|
selectBuilding: (building: Building) => void;
|
||||||
|
user_verified?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,17 +18,22 @@ const CheckboxDataEntry: React.FunctionComponent<CheckboxDataEntryProps> = (prop
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
/>
|
/>
|
||||||
<div className="form-check">
|
<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
|
<label
|
||||||
htmlFor={props.slug}
|
htmlFor={props.slug}
|
||||||
className="form-check-label">
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
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 React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
|
import Verification from './verification';
|
||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../data_fields';
|
||||||
import { CopyProps } from '../data-containers/category-view-props';
|
import { CopyProps } from '../data-containers/category-view-props';
|
||||||
|
|
||||||
@ -12,6 +13,12 @@ interface YearDataEntryProps {
|
|||||||
copy?: CopyProps;
|
copy?: CopyProps;
|
||||||
mode?: 'view' | 'edit' | 'multi-edit';
|
mode?: 'view' | 'edit' | 'multi-edit';
|
||||||
onChange?: (key: string, value: any) => void;
|
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> {
|
class YearDataEntry extends Component<YearDataEntryProps, any> {
|
||||||
@ -45,6 +52,15 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
|
|||||||
max={currentYear}
|
max={currentYear}
|
||||||
// "type": "year_estimator"
|
// "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
|
<NumericDataEntry
|
||||||
title={dataFields.date_upper.title}
|
title={dataFields.date_upper.title}
|
||||||
slug="date_upper"
|
slug="date_upper"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { NavLink, Redirect } from 'react-router-dom';
|
import { NavLink, Redirect } from 'react-router-dom';
|
||||||
|
import Confetti from 'canvas-confetti';
|
||||||
|
|
||||||
import { apiPost } from '../apiHelpers';
|
import { apiPost } from '../apiHelpers';
|
||||||
import ErrorBox from '../components/error-box';
|
import ErrorBox from '../components/error-box';
|
||||||
@ -24,6 +25,7 @@ interface DataContainerProps {
|
|||||||
mode: 'view' | 'edit';
|
mode: 'view' | 'edit';
|
||||||
building?: Building;
|
building?: Building;
|
||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
|
user_verified?: any;
|
||||||
selectBuilding: (building: Building) => void;
|
selectBuilding: (building: Building) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
this.handleReset = this.handleReset.bind(this);
|
this.handleReset = this.handleReset.bind(this);
|
||||||
this.handleLike = this.handleLike.bind(this);
|
this.handleLike = this.handleLike.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleVerify = this.handleVerify.bind(this);
|
||||||
|
|
||||||
this.toggleCopying = this.toggleCopying.bind(this);
|
this.toggleCopying = this.toggleCopying.bind(this);
|
||||||
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||||
@ -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() {
|
render() {
|
||||||
if (this.props.mode === 'edit' && !this.props.user){
|
if (this.props.mode === 'edit' && !this.props.user){
|
||||||
return <Redirect to="/sign-up.html" />;
|
return <Redirect to="/sign-up.html" />;
|
||||||
@ -281,9 +316,12 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
building={undefined}
|
building={undefined}
|
||||||
building_like={undefined}
|
building_like={undefined}
|
||||||
mode={this.props.mode}
|
mode={this.props.mode}
|
||||||
|
edited={false}
|
||||||
copy={copy}
|
copy={copy}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onLike={this.handleLike}
|
onLike={this.handleLike}
|
||||||
|
onVerify={this.handleVerify}
|
||||||
|
user_verified={[]}
|
||||||
/>
|
/>
|
||||||
</Fragment> :
|
</Fragment> :
|
||||||
this.props.building != undefined ?
|
this.props.building != undefined ?
|
||||||
@ -327,9 +365,13 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
|||||||
building={currentBuilding}
|
building={currentBuilding}
|
||||||
building_like={this.props.building_like}
|
building_like={this.props.building_like}
|
||||||
mode={this.props.mode}
|
mode={this.props.mode}
|
||||||
|
edited={edited}
|
||||||
copy={copy}
|
copy={copy}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onLike={this.handleLike}
|
onLike={this.handleLike}
|
||||||
|
onVerify={this.handleVerify}
|
||||||
|
user_verified={this.props.user_verified}
|
||||||
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
</form> :
|
</form> :
|
||||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
<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 NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
import TextboxDataEntry from '../data-components/textbox-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 YearDataEntry from '../data-components/year-data-entry';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
@ -25,6 +26,12 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.facade_year.title}
|
title={dataFields.facade_year.title}
|
||||||
@ -38,6 +45,15 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
max={currentYear}
|
max={currentYear}
|
||||||
tooltip={dataFields.facade_year.tooltip}
|
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
|
<SelectDataEntry
|
||||||
title={dataFields.date_source.title}
|
title={dataFields.date_source.title}
|
||||||
slug="date_source"
|
slug="date_source"
|
||||||
|
@ -10,9 +10,13 @@ interface CategoryViewProps {
|
|||||||
building: any; // TODO: add Building type with all fields
|
building: any; // TODO: add Building type with all fields
|
||||||
building_like: boolean;
|
building_like: boolean;
|
||||||
mode: 'view' | 'edit' | 'multi-edit';
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
|
edited: boolean;
|
||||||
copy: CopyProps;
|
copy: CopyProps;
|
||||||
onChange: (key: string, value: any) => void;
|
onChange: (key: string, value: any) => void;
|
||||||
onLike: (like: boolean) => void;
|
onLike: (like: boolean) => void;
|
||||||
|
onVerify: (slug: string, verify: boolean, x: number, y: number) => void;
|
||||||
|
user_verified: any;
|
||||||
|
user?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -5,6 +5,7 @@ import { dataFields } from '../../data_fields';
|
|||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import UPRNsDataEntry from '../data-components/uprns-data-entry';
|
import UPRNsDataEntry from '../data-components/uprns-data-entry';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
@ -21,8 +22,16 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
tooltip={dataFields.location_name.tooltip}
|
tooltip={dataFields.location_name.tooltip}
|
||||||
placeholder="Building name (if any)"
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.location_number.title}
|
title={dataFields.location_number.title}
|
||||||
slug="location_number"
|
slug="location_number"
|
||||||
@ -33,6 +42,15 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={1}
|
step={1}
|
||||||
min={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
|
<DataEntry
|
||||||
title={dataFields.location_street.title}
|
title={dataFields.location_street.title}
|
||||||
slug="location_street"
|
slug="location_street"
|
||||||
@ -40,8 +58,16 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.location_line_two.title}
|
title={dataFields.location_line_two.title}
|
||||||
slug="location_line_two"
|
slug="location_line_two"
|
||||||
|
@ -6,6 +6,7 @@ import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
|||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
@ -23,6 +24,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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">
|
<DataEntryGroup name="Planning Status">
|
||||||
<CheckboxDataEntry
|
<CheckboxDataEntry
|
||||||
title="Is a planning application live for this site?"
|
title="Is a planning application live for this site?"
|
||||||
@ -84,6 +94,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_conservation_area_name.title}
|
title={dataFields.planning_conservation_area_name.title}
|
||||||
slug="planning_conservation_area_name"
|
slug="planning_conservation_area_name"
|
||||||
@ -92,6 +110,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<CheckboxDataEntry
|
||||||
title={dataFields.planning_in_list.title}
|
title={dataFields.planning_in_list.title}
|
||||||
slug="planning_in_list"
|
slug="planning_in_list"
|
||||||
@ -149,6 +176,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_world_list_id.title}
|
title={dataFields.planning_world_list_id.title}
|
||||||
slug="planning_world_list_id"
|
slug="planning_world_list_id"
|
||||||
@ -157,6 +193,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<CheckboxDataEntry
|
||||||
title={dataFields.planning_in_glher.title}
|
title={dataFields.planning_in_glher.title}
|
||||||
slug="planning_in_glher"
|
slug="planning_in_glher"
|
||||||
@ -165,6 +210,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_glher_url.title}
|
title={dataFields.planning_glher_url.title}
|
||||||
slug="planning_glher_url"
|
slug="planning_glher_url"
|
||||||
@ -173,6 +226,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<CheckboxDataEntry
|
||||||
title={dataFields.planning_in_apa.title}
|
title={dataFields.planning_in_apa.title}
|
||||||
slug="planning_in_apa"
|
slug="planning_in_apa"
|
||||||
@ -181,6 +243,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_apa_name.title}
|
title={dataFields.planning_apa_name.title}
|
||||||
slug="planning_apa_name"
|
slug="planning_apa_name"
|
||||||
@ -189,6 +259,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_apa_tier.title}
|
title={dataFields.planning_apa_tier.title}
|
||||||
slug="planning_apa_tier"
|
slug="planning_apa_tier"
|
||||||
@ -197,6 +275,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<CheckboxDataEntry
|
||||||
title={dataFields.planning_in_local_list.title}
|
title={dataFields.planning_in_local_list.title}
|
||||||
slug="planning_in_local_list"
|
slug="planning_in_local_list"
|
||||||
@ -205,6 +292,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_local_list_url.title}
|
title={dataFields.planning_local_list_url.title}
|
||||||
slug="planning_local_list_url"
|
slug="planning_local_list_url"
|
||||||
@ -213,6 +308,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<CheckboxDataEntry
|
||||||
title={dataFields.planning_in_historic_area_assessment.title}
|
title={dataFields.planning_in_historic_area_assessment.title}
|
||||||
slug="planning_in_historic_area_assessment"
|
slug="planning_in_historic_area_assessment"
|
||||||
@ -221,6 +325,14 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<DataEntry
|
||||||
title={dataFields.planning_historic_area_assessment_url.title}
|
title={dataFields.planning_historic_area_assessment_url.title}
|
||||||
slug="planning_historic_area_assessment_url"
|
slug="planning_historic_area_assessment_url"
|
||||||
@ -229,6 +341,15 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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>
|
</DataEntryGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import { dataFields } from '../../data_fields';
|
|||||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
@ -14,7 +15,6 @@ import { CategoryViewProps } from './category-view-props';
|
|||||||
const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DataEntryGroup name="Storeys">
|
<DataEntryGroup name="Storeys">
|
||||||
|
|
||||||
<NumericDataEntry
|
<NumericDataEntry
|
||||||
title={dataFields.size_storeys_core.title}
|
title={dataFields.size_storeys_core.title}
|
||||||
slug="size_storeys_core"
|
slug="size_storeys_core"
|
||||||
@ -26,6 +26,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={1}
|
step={1}
|
||||||
min={0}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.size_storeys_attic.title}
|
title={dataFields.size_storeys_attic.title}
|
||||||
slug="size_storeys_attic"
|
slug="size_storeys_attic"
|
||||||
@ -37,6 +46,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={1}
|
step={1}
|
||||||
min={0}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.size_storeys_basement.title}
|
title={dataFields.size_storeys_basement.title}
|
||||||
slug="size_storeys_basement"
|
slug="size_storeys_basement"
|
||||||
@ -48,6 +66,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={1}
|
step={1}
|
||||||
min={0}
|
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>
|
||||||
<DataEntryGroup name="Height" collapsed={false}>
|
<DataEntryGroup name="Height" collapsed={false}>
|
||||||
<NumericDataEntry
|
<NumericDataEntry
|
||||||
@ -60,6 +87,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
min={0}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.size_height_eaves.title}
|
title={dataFields.size_height_eaves.title}
|
||||||
slug="size_height_eaves"
|
slug="size_height_eaves"
|
||||||
@ -83,6 +119,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
min={0}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.size_floor_area_total.title}
|
title={dataFields.size_floor_area_total.title}
|
||||||
slug="size_floor_area_total"
|
slug="size_floor_area_total"
|
||||||
@ -93,6 +138,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
min={0}
|
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>
|
</DataEntryGroup>
|
||||||
<NumericDataEntry
|
<NumericDataEntry
|
||||||
title={dataFields.size_width_frontage.title}
|
title={dataFields.size_width_frontage.title}
|
||||||
@ -104,6 +158,15 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
min={0}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.size_plot_area_total.title}
|
title={dataFields.size_plot_area_total.title}
|
||||||
slug="size_plot_area_total"
|
slug="size_plot_area_total"
|
||||||
|
@ -3,6 +3,7 @@ import React, { Fragment } from 'react';
|
|||||||
import { dataFields } from '../../data_fields';
|
import { dataFields } from '../../data_fields';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
@ -32,6 +33,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<SelectDataEntry
|
||||||
title={dataFields.sust_dec.title}
|
title={dataFields.sust_dec.title}
|
||||||
slug="sust_dec"
|
slug="sust_dec"
|
||||||
@ -42,6 +52,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<SelectDataEntry
|
||||||
title={dataFields.sust_aggregate_estimate_epc.title}
|
title={dataFields.sust_aggregate_estimate_epc.title}
|
||||||
slug="sust_aggregate_estimate_epc"
|
slug="sust_aggregate_estimate_epc"
|
||||||
@ -53,6 +72,7 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NumericDataEntry
|
<NumericDataEntry
|
||||||
title={dataFields.sust_retrofit_date.title}
|
title={dataFields.sust_retrofit_date.title}
|
||||||
slug="sust_retrofit_date"
|
slug="sust_retrofit_date"
|
||||||
@ -65,6 +85,15 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.sust_life_expectancy.title}
|
title={dataFields.sust_life_expectancy.title}
|
||||||
slug="sust_life_expectancy"
|
slug="sust_life_expectancy"
|
||||||
|
@ -4,6 +4,7 @@ import { dataFields } from '../../data_fields';
|
|||||||
import DataEntry from '../data-components/data-entry';
|
import DataEntry from '../data-components/data-entry';
|
||||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
@ -31,6 +32,15 @@ const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
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
|
<NumericDataEntry
|
||||||
title={dataFields.date_change_building_use.title}
|
title={dataFields.date_change_building_use.title}
|
||||||
slug="date_change_building_use"
|
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 withCopyEdit from '../data-container';
|
||||||
|
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
|
import Verification from '../data-components/verification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use view/edit section
|
* Use view/edit section
|
||||||
@ -29,6 +30,14 @@ const UseView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
showAllOptionsOnEmpty={true}
|
showAllOptionsOnEmpty={true}
|
||||||
addOnAutofillSelect={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' &&
|
props.mode != 'view' &&
|
||||||
<InfoBox msg="Land use order, shown below, is automatically derived from the land use groups"></InfoBox>
|
<InfoBox msg="Land use order, shown below, is automatically derived from the land use groups"></InfoBox>
|
||||||
|
@ -2,8 +2,22 @@
|
|||||||
* Mini-library of icons
|
* Mini-library of icons
|
||||||
*/
|
*/
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faAngleLeft, faAngleRight, faCaretDown, faCaretRight, faCaretUp, faCheck, faCheckDouble,
|
import {
|
||||||
faEye, faInfoCircle, faPaintBrush, faQuestionCircle, faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -13,6 +27,7 @@ library.add(
|
|||||||
faPaintBrush,
|
faPaintBrush,
|
||||||
faTimes,
|
faTimes,
|
||||||
faCheck,
|
faCheck,
|
||||||
|
faCheckCircle,
|
||||||
faCheckDouble,
|
faCheckDouble,
|
||||||
faAngleLeft,
|
faAngleLeft,
|
||||||
faAngleRight,
|
faAngleRight,
|
||||||
@ -51,6 +66,10 @@ const SaveDoneIcon = () => (
|
|||||||
<FontAwesomeIcon icon="check-double" />
|
<FontAwesomeIcon icon="check-double" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const VerifyIcon = () => (
|
||||||
|
<FontAwesomeIcon icon="check-circle" />
|
||||||
|
)
|
||||||
|
|
||||||
const BackIcon = () => (
|
const BackIcon = () => (
|
||||||
<FontAwesomeIcon icon="angle-left" />
|
<FontAwesomeIcon icon="angle-left" />
|
||||||
);
|
);
|
||||||
@ -88,5 +107,6 @@ export {
|
|||||||
DownIcon,
|
DownIcon,
|
||||||
UpIcon,
|
UpIcon,
|
||||||
RightIcon,
|
RightIcon,
|
||||||
SearchIcon
|
SearchIcon,
|
||||||
|
VerifyIcon
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,7 @@ interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
|||||||
building_like?: boolean;
|
building_like?: boolean;
|
||||||
user?: any;
|
user?: any;
|
||||||
revisionId?: number;
|
revisionId?: number;
|
||||||
|
user_verified?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapAppState {
|
interface MapAppState {
|
||||||
@ -33,6 +34,7 @@ interface MapAppState {
|
|||||||
revision_id: number;
|
revision_id: number;
|
||||||
building: Building;
|
building: Building;
|
||||||
building_like: boolean;
|
building_like: boolean;
|
||||||
|
user_verified: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
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),
|
category: this.getCategory(props.match.params.category),
|
||||||
revision_id: props.revisionId || 0,
|
revision_id: props.revisionId || 0,
|
||||||
building: props.building,
|
building: props.building,
|
||||||
building_like: props.building_like
|
building_like: props.building_like,
|
||||||
|
user_verified: props.user_verified || {}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.selectBuilding = this.selectBuilding.bind(this);
|
this.selectBuilding = this.selectBuilding.bind(this);
|
||||||
@ -60,7 +63,10 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchLatestRevision();
|
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() {
|
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
|
* Fetches building data if a building is selected but no data provided through
|
||||||
* props (from server-side rendering)
|
* props (from server-side rendering)
|
||||||
*/
|
*/
|
||||||
async fetchBuildingData() {
|
async fetchBuildingData(buildingId: number) {
|
||||||
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
try {
|
||||||
try {
|
// TODO: simplify API calls, create helpers for fetching data
|
||||||
// TODO: simplify API calls, create helpers for fetching data
|
let [building, building_uprns, building_like, user_verified] = await Promise.all([
|
||||||
const buildingId = strictParseInt(this.props.match.params.building);
|
apiGet(`/api/buildings/${buildingId}.json`),
|
||||||
let [building, building_uprns, building_like] = await Promise.all([
|
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||||
apiGet(`/api/buildings/${buildingId}.json`),
|
apiGet(`/api/buildings/${buildingId}/like.json`),
|
||||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
apiGet(`/api/buildings/${buildingId}/verify.json`)
|
||||||
apiGet(`/api/buildings/${buildingId}/like.json`)
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
building.uprns = building_uprns.uprns;
|
building.uprns = building_uprns.uprns;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
building: building,
|
building: building,
|
||||||
building_like: building_like.like
|
building_like: building_like.like,
|
||||||
});
|
user_verified: user_verified
|
||||||
} catch(error) {
|
});
|
||||||
console.error(error);
|
|
||||||
// TODO: add UI for API errors
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.increaseRevision(building.revision_id);
|
this.fetchBuildingData(building.building_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 });
|
|
||||||
});
|
|
||||||
|
|
||||||
// get if liked and update
|
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,6 +231,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
|||||||
cat={category}
|
cat={category}
|
||||||
building={this.state.building}
|
building={this.state.building}
|
||||||
building_like={this.state.building_like}
|
building_like={this.state.building_like}
|
||||||
|
user_verified={this.state.user_verified}
|
||||||
selectBuilding={this.selectBuilding}
|
selectBuilding={this.selectBuilding}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
|
@ -76,6 +76,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-legend li {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.data-legend .key {
|
.data-legend .key {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.3rem;
|
width: 1.3rem;
|
||||||
|
@ -228,14 +228,8 @@ class Legend extends React.Component<LegendProps, LegendState> {
|
|||||||
return (
|
return (
|
||||||
|
|
||||||
<li key={item.color} >
|
<li key={item.color} >
|
||||||
<tr>
|
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
||||||
<td>
|
{ item.text }
|
||||||
<div className="key" style={ { background: item.color, border: item.border } }></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{ item.text }
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -8,18 +8,20 @@ import {
|
|||||||
getBuildingById,
|
getBuildingById,
|
||||||
getBuildingLikeById,
|
getBuildingLikeById,
|
||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
getLatestRevisionId
|
getLatestRevisionId,
|
||||||
|
getUserVerifiedAttributes
|
||||||
} from './api/services/building';
|
} from './api/services/building';
|
||||||
import { getUserById } from './api/services/user';
|
import { getUserById } from './api/services/user';
|
||||||
import App from './frontend/app';
|
import App from './frontend/app';
|
||||||
import { parseBuildingURL } from './parse';
|
import { parseBuildingURL } from './parse';
|
||||||
|
import asyncController from './api/routes/asyncController';
|
||||||
|
|
||||||
|
|
||||||
// reference packed assets
|
// reference packed assets
|
||||||
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
|
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 context: any = {}; // TODO: remove any
|
||||||
const data: any = {}; // TODO: remove any
|
const data: any = {}; // TODO: remove any
|
||||||
context.status = 200;
|
context.status = 200;
|
||||||
@ -31,34 +33,39 @@ function frontendRoute(req: express.Request, res: express.Response) {
|
|||||||
context.status = 404;
|
context.status = 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all([
|
try {
|
||||||
userId ? getUserById(userId) : undefined,
|
let [user, building, uprns, buildingLike, userVerified, latestRevisionId] = await Promise.all([
|
||||||
isBuilding ? getBuildingById(buildingId) : undefined,
|
userId ? getUserById(userId) : undefined,
|
||||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
isBuilding ? getBuildingById(buildingId) : undefined,
|
||||||
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||||
getLatestRevisionId()
|
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false,
|
||||||
]).then(function ([user, building, uprns, buildingLike, latestRevisionId]) {
|
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||||
|
getLatestRevisionId()
|
||||||
|
]);
|
||||||
|
|
||||||
if (isBuilding && typeof (building) === 'undefined') {
|
if (isBuilding && typeof (building) === 'undefined') {
|
||||||
context.status = 404;
|
context.status = 404;
|
||||||
}
|
}
|
||||||
data.user = user;
|
data.user = user;
|
||||||
data.building = building;
|
data.building = building;
|
||||||
data.building_like = buildingLike;
|
data.building_like = buildingLike;
|
||||||
|
data.user_verified = userVerified;
|
||||||
if (data.building != null) {
|
if (data.building != null) {
|
||||||
data.building.uprns = uprns;
|
data.building.uprns = uprns;
|
||||||
}
|
}
|
||||||
data.latestRevisionId = latestRevisionId;
|
data.latestRevisionId = latestRevisionId;
|
||||||
renderHTML(context, data, req, res);
|
renderHTML(context, data, req, res);
|
||||||
}).catch(error => {
|
} catch(error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
data.user = undefined;
|
data.user = undefined;
|
||||||
data.building = undefined;
|
data.building = undefined;
|
||||||
data.building_like = undefined;
|
data.building_like = undefined;
|
||||||
|
data.user_verified = {}
|
||||||
data.latestRevisionId = 0;
|
data.latestRevisionId = 0;
|
||||||
context.status = 500;
|
context.status = 500;
|
||||||
renderHTML(context, data, req, res);
|
renderHTML(context, data, req, res);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function renderHTML(context, data, req, res) {
|
function renderHTML(context, data, req, res) {
|
||||||
const markup = renderToString(
|
const markup = renderToString(
|
||||||
|
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;
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO appusername;
|
||||||
-- read map search locations
|
-- read map search locations
|
||||||
GRANT SELECT ON TABLE search_locations to appusername;
|
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:
|
Set or update passwords:
|
||||||
|
Loading…
Reference in New Issue
Block a user