Merge pull request #625 from colouring-london/feature/verification

Feature/verification
This commit is contained in:
Tom Russell 2020-08-07 14:53:17 +01:00 committed by GitHub
commit 488a5da1bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2209 additions and 4277 deletions

5536
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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
};

View 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);
}
}

View File

@ -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);

View File

@ -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
};

View File

@ -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

View File

@ -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>,

View File

@ -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}
/>

View File

@ -24,6 +24,7 @@ interface BuildingViewProps {
building_like?: boolean;
user?: any;
selectBuilding: (building: Building) => void;
user_verified?: any;
}
/**

View File

@ -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>

View File

@ -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}

View File

@ -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)

View 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;
}

View 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;

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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 {

View File

@ -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"

View File

@ -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>
);

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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
};

View File

@ -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}
/>

View File

@ -76,6 +76,10 @@
flex: 1;
}
.data-legend li {
white-space: nowrap;
}
.data-legend .key {
display: inline-block;
width: 1.3rem;

View File

@ -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>
);

View File

@ -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">

View File

@ -0,0 +1,2 @@
-- Building verification
DROP TABLE IF EXISTS building_verification;

View 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 );

View File

@ -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: