Merge branch 'master' into feature/500-age-source-update

This commit is contained in:
Maciej Ziarkowski 2019-12-02 00:12:55 +00:00
commit 2428b28eda
20 changed files with 207 additions and 138 deletions

View File

@ -6,7 +6,7 @@ async function getGlobalEditHistory() {
`SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username, building_id
FROM logs, users
WHERE logs.user_id = users.user_id
AND log_timestamp >= now() - interval '21 days'
AND log_timestamp >= now() - interval '7 days'
ORDER BY log_timestamp DESC`
);
} catch (error) {

View File

@ -60,7 +60,10 @@ async function authUser(username: string, password: string) {
user_id,
(
pass = crypt($2, pass)
) AS auth_ok
) AS auth_ok,
is_blocked,
blocked_on,
blocked_reason
FROM users
WHERE
username = $1
@ -71,6 +74,9 @@ async function authUser(username: string, password: string) {
);
if (user && user.auth_ok) {
if (user.is_blocked) {
return { error: `Account temporarily blocked.${user.blocked_reason == undefined ? '' : ' Reason: '+user.blocked_reason}` };
}
return { user_id: user.user_id };
} else {
return { error: 'Username or password not recognised' };

View File

@ -14,7 +14,7 @@ const CheckboxDataEntry: React.FunctionComponent<CheckboxDataEntryProps> = (prop
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined}
copy={props.copy}
/>
<div className="form-check">

View File

@ -28,7 +28,7 @@ const DataEntry: React.FunctionComponent<DataEntryProps> = (props) => {
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined || props.value == ''}
copy={props.copy}
/>
<input className="form-control" type="text"

View File

@ -57,7 +57,7 @@ class MultiDataEntry extends Component<MultiDataEntryProps> {
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined || props.value.length === 0}
/>
{
(props.mode === 'view')?

View File

@ -19,7 +19,7 @@ const NumericDataEntry: React.FunctionComponent<NumericDataEntryProps> = (props)
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined}
copy={props.copy}
/>
<input
@ -27,10 +27,10 @@ const NumericDataEntry: React.FunctionComponent<NumericDataEntryProps> = (props)
type="number"
id={props.slug}
name={props.slug}
value={props.value || ''}
step={props.step || 1}
value={props.value == undefined ? '' : props.value}
step={props.step == undefined ? 1 : props.step}
max={props.max}
min={props.min || 0}
min={props.min}
disabled={props.mode === 'view' || props.disabled}
placeholder={props.placeholder}
onChange={e =>

View File

@ -17,7 +17,7 @@ const SelectDataEntry: React.FunctionComponent<SelectDataEntryProps> = (props) =
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined}
copy={props.copy}
/>
<select className="form-control"

View File

@ -16,7 +16,7 @@ const TextboxDataEntry: React.FunctionComponent<TextboxDataEntryProps> = (props)
slug={props.slug}
title={props.title}
tooltip={props.tooltip}
disabled={props.disabled}
disabled={props.disabled || props.value == undefined}
copy={props.copy}
/>
<textarea

View File

@ -30,6 +30,8 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
// TODO handle changes internally, reporting out date_year, date_upper, date_lower
render() {
const props = this.props;
const currentYear = new Date().getFullYear();
return (
<Fragment>
<NumericDataEntry
@ -39,6 +41,8 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
min={1}
max={currentYear}
// "type": "year_estimator"
/>
<NumericDataEntry
@ -49,6 +53,8 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
copy={props.copy}
onChange={props.onChange}
step={1}
min={1}
max={currentYear}
tooltip={dataFields.date_upper.tooltip}
/>
<NumericDataEntry
@ -59,6 +65,8 @@ class YearDataEntry extends Component<YearDataEntryProps, any> {
copy={props.copy}
onChange={props.onChange}
step={1}
min={1}
max={currentYear}
tooltip={dataFields.date_lower.tooltip}
/>
</Fragment>

View File

@ -13,73 +13,79 @@ import { CategoryViewProps } from './category-view-props';
/**
* Age view/edit section
*/
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => (
<Fragment>
<YearDataEntry
year={props.building.date_year}
upper={props.building.date_upper}
lower={props.building.date_lower}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
/>
<NumericDataEntry
title={dataFields.facade_year.title}
slug="facade_year"
value={props.building.facade_year}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
step={1}
tooltip={dataFields.facade_year.tooltip}
/>
<SelectDataEntry
title={dataFields.date_source.title}
slug="date_source"
value={props.building.date_source}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_source.tooltip}
placeholder=""
options={[
"Expert knowledge of building",
"Expert estimate from image",
"Survey of London",
"Pevsner Guides",
"Victoria County History",
"Local history publication",
"Other publication",
"National Heritage List for England",
"Other database or gazetteer",
"Historical map",
"Other archive document",
"Film/Video",
"Other website",
"Other"
]}
/>
<TextboxDataEntry
title={dataFields.date_source_detail.title}
slug="date_source_detail"
value={props.building.date_source_detail}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_source_detail.tooltip}
/>
<MultiDataEntry
title={dataFields.date_link.title}
slug="date_link"
value={props.building.date_link}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_link.tooltip}
placeholder="https://..."
/>
</Fragment>
);
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
const currentYear = new Date().getFullYear();
return (
<Fragment>
<YearDataEntry
year={props.building.date_year}
upper={props.building.date_upper}
lower={props.building.date_lower}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
/>
<NumericDataEntry
title={dataFields.facade_year.title}
slug="facade_year"
value={props.building.facade_year}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
step={1}
min={1}
max={currentYear}
tooltip={dataFields.facade_year.tooltip}
/>
<SelectDataEntry
title={dataFields.date_source.title}
slug="date_source"
value={props.building.date_source}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_source.tooltip}
placeholder=""
options={[
"Expert knowledge of building",
"Expert estimate from image",
"Survey of London",
"Pevsner Guides",
"Victoria County History",
"Local history publication",
"Other publication",
"National Heritage List for England",
"Other database or gazetteer",
"Historical map",
"Other archive document",
"Film/Video",
"Other website",
"Other"
]}
/>
<TextboxDataEntry
title={dataFields.date_source_detail.title}
slug="date_source_detail"
value={props.building.date_source_detail}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_source_detail.tooltip}
/>
<MultiDataEntry
title={dataFields.date_link.title}
slug="date_link"
value={props.building.date_link}
mode={props.mode}
copy={props.copy}
onChange={props.onChange}
tooltip={dataFields.date_link.tooltip}
placeholder="https://..."
/>
</Fragment>
);
};
const AgeContainer = withCopyEdit(AgeView);
export default AgeContainer;

View File

@ -31,6 +31,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={1}
min={1}
/>
<DataEntry
title={dataFields.location_street.title}
@ -99,8 +100,10 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
value={props.building.location_latitude}
mode={props.mode}
copy={props.copy}
step={0.0001}
placeholder="51"
step={0.00001}
min={-90}
max={90}
placeholder="Latitude, e.g. 51.5467"
onChange={props.onChange}
/>
<NumericDataEntry
@ -109,8 +112,10 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
value={props.building.location_longitude}
mode={props.mode}
copy={props.copy}
step={0.0001}
placeholder="0"
step={0.00001}
min={-180}
max={180}
placeholder="Longitude, e.g. -0.0586"
onChange={props.onChange}
/>
</Fragment>

View File

@ -24,6 +24,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
tooltip={dataFields.size_storeys_core.tooltip}
onChange={props.onChange}
step={1}
min={0}
/>
<NumericDataEntry
title={dataFields.size_storeys_attic.title}
@ -34,6 +35,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
tooltip={dataFields.size_storeys_attic.tooltip}
onChange={props.onChange}
step={1}
min={0}
/>
<NumericDataEntry
title={dataFields.size_storeys_basement.title}
@ -44,6 +46,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
tooltip={dataFields.size_storeys_basement.tooltip}
onChange={props.onChange}
step={1}
min={0}
/>
</DataEntryGroup>
<DataEntryGroup name="Height">
@ -55,6 +58,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
/>
<NumericDataEntry
title={dataFields.size_height_eaves.title}
@ -65,6 +69,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
/>
</DataEntryGroup>
<DataEntryGroup name="Floor area">
@ -76,6 +81,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
/>
<NumericDataEntry
title={dataFields.size_floor_area_total.title}
@ -85,6 +91,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
/>
</DataEntryGroup>
<NumericDataEntry
@ -95,6 +102,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
/>
<NumericDataEntry
title={dataFields.size_plot_area_total.title}
@ -104,6 +112,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
disabled={true}
/>
<NumericDataEntry
@ -114,6 +123,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
copy={props.copy}
onChange={props.onChange}
step={0.1}
min={0}
disabled={true}
/>
<SelectDataEntry

View File

@ -2,6 +2,8 @@ import { parse } from 'query-string';
import React from 'react';
import { Link, Redirect, RouteComponentProps } from 'react-router-dom';
import { parseJsonOrDefault } from '../../helpers';
import ErrorBox from '../components/error-box';
import { BackIcon } from '../components/icons';
import InfoBox from '../components/info-box';
import { dataFields } from '../data_fields';
@ -10,25 +12,22 @@ import { User } from '../models/user';
import DataEntry from './data-components/data-entry';
import Sidebar from './sidebar';
interface MultiEditRouteParams {
cat: string;
}
interface MultiEditProps extends RouteComponentProps<MultiEditRouteParams> {
interface MultiEditProps {
user?: User;
category: string;
dataString: string;
}
const MultiEdit: React.FC<MultiEditProps> = (props) => {
if (!props.user){
return <Redirect to="/sign-up.html" />;
}
const cat = props.match.params.cat;
if (cat === 'like') {
if (props.category === 'like') {
// special case for likes
return (
<Sidebar>
<section className='data-section'>
<header className={`section-header view ${cat} background-${cat}`}>
<header className={`section-header view ${props.category} background-${props.category}`}>
<h2 className="h2">Like me!</h2>
</header>
<form className='buttons-container'>
@ -42,34 +41,34 @@ const MultiEdit: React.FC<MultiEditProps> = (props) => {
);
}
const q = parse(props.location.search);
let data = parseJsonOrDefault(props.dataString);
let data: object;
if (cat === 'like'){
data = { like: true };
} else {
try {
// TODO: verify what happens if data is string[]
data = JSON.parse(q.data as string);
} catch (error) {
console.error(error, q);
data = {};
}
let error: string;
if(data == null) {
error = 'Invalid parameters supplied';
data = {};
} else if(Object.values(data).some(x => x == undefined)) {
error = 'Cannot copy empty values';
data = {};
}
return (
<Sidebar>
<section className='data-section'>
<header className={`section-header view ${cat} background-${cat}`}>
<header className={`section-header view ${props.category} background-${props.category}`}>
<Link
className="icon-button back"
to={`/edit/${cat}`}>
to={`/edit/${props.category}`}>
<BackIcon />
</Link>
<h2 className="h2">Copy {cat} data</h2>
<h2 className="h2">Copy {props.category} data</h2>
</header>
<form>
<InfoBox msg='Click buildings one at a time to colour using the data below' />
{
error ?
<ErrorBox msg={error} /> :
<InfoBox msg='Click buildings one at a time to colour using the data below' />
}
{
Object.keys(data).map((key => {
const info = dataFields[key] || {};
@ -85,8 +84,8 @@ const MultiEdit: React.FC<MultiEditProps> = (props) => {
}
</form>
<form className='buttons-container'>
<Link to={`/view/${cat}`} className='btn btn-secondary'>Back to view</Link>
<Link to={`/edit/${cat}`} className='btn btn-secondary'>Back to edit</Link>
<Link to={`/view/${props.category}`} className='btn btn-secondary'>Back to view</Link>
<Link to={`/edit/${props.category}`} className='btn btn-secondary'>Back to edit</Link>
</form>
</section>
</Sidebar>

View File

@ -1,7 +1,8 @@
import { parse } from 'query-string';
import { parse as parseQuery } from 'query-string';
import React, { Fragment } from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { parseJsonOrDefault } from '../helpers';
import { strictParseInt } from '../parse';
import BuildingView from './building/building-view';
@ -130,6 +131,13 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
return category;
}
getMultiEditDataString(): string {
const q = parseQuery(this.props.location.search);
if(Array.isArray(q.data)) {
throw new Error('Invalid format');
} else return q.data;
}
increaseRevision(revisionId) {
revisionId = +revisionId;
// bump revision id, only ever increasing
@ -199,21 +207,19 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
*
* Pulls data from URL to form update
*
* @param {object} building
* @param {Building} building
*/
colourBuilding(building) {
colourBuilding(building: Building) {
const cat = this.props.match.params.category;
const q = parse(window.location.search);
if (cat === 'like') {
this.likeBuilding(building.building_id);
} else {
try {
// TODO: verify what happens if data is string[]
const data = JSON.parse(q.data as string);
const data = parseJsonOrDefault(this.getMultiEditDataString());
if (data != undefined && !Object.values(data).some(x => x == undefined)) {
this.updateBuilding(building.building_id, data);
} catch (error) {
console.error(error, q);
}
}
}
@ -281,7 +287,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
</Route>
<Route exact path="/multi-edit/:cat" render={(props) => (
<MultiEdit
{...props}
category={category}
dataString={this.getMultiEditDataString()}
user={this.props.user}
/>
)} />

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { BuildingEditSummary } from '../building/edit-history/building-edit-summary';
import InfoBox from '../components/info-box';
import { EditHistoryEntry } from '../models/edit-history-entry';
const ChangesPage = () => {
@ -23,15 +24,18 @@ const ChangesPage = () => {
<h1>Global edit history</h1>
<ul className="edit-history-list">
{history && history.map(entry => (
<li key={`${entry.revision_id}`} className="edit-history-list-element">
<BuildingEditSummary
historyEntry={entry}
showBuildingId={true}
hyperlinkCategories={true}
/>
</li>
))}
{(history == undefined || history.length == 0) ?
<InfoBox msg="No changes in the last week"></InfoBox> :
history.map(entry => (
<li key={`${entry.revision_id}`} className="edit-history-list-element">
<BuildingEditSummary
historyEntry={entry}
showBuildingId={true}
hyperlinkCategories={true}
/>
</li>
))
}
</ul>
</section>
</article>

View File

@ -6,12 +6,14 @@
z-index: 10000;
top: 0;
width: 100%;
max-height: 100%;
max-height: 95%;
max-height: calc(100%-2em);
border-radius: 0;
padding: 1.5em 2.5em 2.5em;
overflow-y: auto;
}
.welcome-float.jumbotron {
padding: 1em 2.5em 1.5em;
background: #fff;
background-color: rgba(255,255,255,0.95);
}
@ -23,3 +25,12 @@
top: 1em;
}
}
.welcome-float .lead {
font-size: 1.2em;
}
.welcome-float .lead a {
color: #333;
border-bottom-color: #333;
}

View File

@ -9,18 +9,13 @@ const Welcome = () => (
<p className="lead">
Colouring London is a knowledge exchange platform collecting information on every
building in London, to help make the city more sustainable. We&rsquo;re building it at The
Bartlett Centre for Advanced Spatial Analysis, University College London.
building in London, to help make the city more sustainable. We're developing it at University College London. Can you help us? We're looking for volunteers of all ages and abilities to help test the site and colour the buildings in.
</p>
<p className="lead">
Can you help us? We&rsquo;re still at an early stage of development, and we&rsquo;re looking for
volunteers of all ages and abilities to test and provide feedback on the site as we
build it.
Our building data comes from many different sources. Though we are unable to vouch for their accuracy, we are currently experimenting with a range of features including 'data source', 'edit history', and 'entry verification', to assist you in checking reliability and judging how suitable the data are for your intended use.
</p>
<p className="lead">
All of the data we collect is made <Link to="/data-extracts.html">openly available</Link> &ndash;
please read our <a href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> and
credit Colouring London if you use or share our maps or data.
All data we collect are made <Link to="/data-extracts.html">openly available</Link>. We just ask you to credit Colouring London and read our <a href="https://www.pages.colouring.london/data-ethics">data ethics policy</a> when using or sharing our data, maps or <a href="https://github.com/tomalrussell/colouring-london">code</a>.
</p>
<Link to="/view/categories"
className="btn btn-outline-dark btn-lg btn-block">

View File

@ -13,3 +13,13 @@ export function dateReviver(name, value) {
}
return value;
}
export function parseJsonOrDefault(jsonString: string) {
try {
return JSON.parse(jsonString);
} catch(error) {
console.error(error);
return null;
}
}

View File

@ -0,0 +1,4 @@
ALTER TABLE users
DROP COLUMN is_blocked,
DROP COLUMN blocked_on,
DROP COLUMN blocked_reason;

View File

@ -0,0 +1,4 @@
ALTER TABLE users
ADD COLUMN is_blocked BOOLEAN NOT NULL DEFAULT (false),
ADD COLUMN blocked_on TIMESTAMP WITH TIME ZONE NULL,
ADD COLUMN blocked_reason TEXT NULL;