diff --git a/src/components/TurkHit/defaultForm.js b/src/components/TurkHit/defaultForm.js index 782777cb85d9786916082b8b7c3142cc7b8f7178..1e29326aebc0127e5dbee2f5433ee8eb5f4c24d4 100644 --- a/src/components/TurkHit/defaultForm.js +++ b/src/components/TurkHit/defaultForm.js @@ -1,13 +1,18 @@ // blocpower_id and address will be added from component const defaultTurkHit = { - instructions_text: 'We need to get building dimensions for buildings using Google Earth. Read the instructions below on what measurements need to be taken.', + instructions_text: 'NOTE: This hit may take 1-2 hours to complete on the first try. Subsequent ' + + 'hits should only take 30-45 minutes. Please ensure the uploaded file is of type .xlsx or ' + + 'the hit will automatically be rejected. Please be sure to follow the directions carefully ' + + 'or the hit will be rejected. \n\n We need to get building dimensions for buildings using ' + + 'Google Earth. Read the instructions below on what measurements need to be taken. The ' + + 'address you will use is in the hit title.', instructions_url: 'http://beta.blocpower.us/dimensions/BuildingDimensionsInstructions.pdf', worksheet_url: 'http://beta.blocpower.us/dimensions/BuildingDimensionsTemplate.xlsx', max_assignments: '1', title: 'Measure building dimensions', - description: 'Use google earth to measure building\'s dimensions, windows, and doors', + description: 'Use google earth to measure a building\'s dimensions, windows, and doors', keywords: 'building, dimensions, google earth', - duration: '7200', + duration: '10800', reward: '5', min_file_bytes: 1, max_file_bytes: 50000000, diff --git a/src/components/TurkHit/index.js b/src/components/TurkHit/index.js index bdd13b87cedb8a26b05114bc4d21e9ba01354a1c..7ce0d776c55d4f9647870b9dc636beb5582f83ff 100644 --- a/src/components/TurkHit/index.js +++ b/src/components/TurkHit/index.js @@ -2,6 +2,7 @@ import React, { PropTypes, Component } from 'react'; import defaultForm from './defaultForm'; import './styles.css'; import turkHitPropTypes from '../../containers/Dimensions/propTypes'; +import documentsPropType from '../../containers/Documents/propTypes'; import turkStatus from './turkStatus'; @@ -15,6 +16,19 @@ class TurkHit extends Component { }; } + handleHitDecision = (hit, approve) => { + const { hitDecision } = this.props; + let responseMessage = null; + if (!approve) { + /* eslint-disable no-alert */ + responseMessage = prompt('You must enter an explanation for the rejection'); + if (!responseMessage) { + return; + } + } + hitDecision(hit, approve, responseMessage); + } + renderDefinitions = () => (

Mechanical Turk

@@ -29,56 +43,97 @@ class TurkHit extends Component {
); - renderDownloadLink = () => { - const buildings = this.props.hit.boxBuildingList; - /* eslint-disable jsx-a11y/href-no-hash */ - return ( - 0 ? buildings.slice(-1)[0].url_download : '#'} - > - Download - - ); + renderDownloadLink = (documentKey) => { + const dimensions = this.props.documents.files.buildingDimensions; + const document = dimensions.reduce((acc, val) => { + if (!acc) { + if (val.key === documentKey) { + return val; + } + } + return acc; + }, null); + if (this.props.documents.loading) { + return 'Loading documents...'; + } + if (document) { + return ( +
+ + Download + +
+ ); + } + return (
); } renderCreateHitButton = () => { const { createHitConfirmation, address, building_id } = this.props; - return ( - - ); - } - - renderHitActions = () => { - const { hitDecision, building_id } = this.props; return (
- +
+
); } + renderHitActions = hit => ( +
+ + +
+ ) + + renderOldHits = oldHitList => ( + oldHitList.map((val) => { + const currStatus = turkStatus[val.status_id]; + return ( +
+ + + + + + + +
Status {currStatus.message}
Message {val.response_message}
Creation Date {val.hit_date}
Creator {val.requester_name}
+

+ {this.renderDownloadLink(val.csv_document_key)} +

+
+ ); + }) + ) + render() { const { hit } = this.props; - const { status } = this.props.hit; - let mainContent =
; + const status = hit.status; + let mainContent =
; if (hit.loading) { return (
@@ -94,15 +149,40 @@ class TurkHit extends Component { ); } - if (status !== '') { + + let oldHits = (
); + if (hit.hitData.length > 0 && status !== null) { + const curHit = hit.hitData[0]; const currStatus = turkStatus[status]; + if (hit.hitData.length > 1) { + oldHits = ( +
+

Previously created hits

+
Definitions
+

+ These are hits that are no longer active. +

+ {this.renderOldHits(hit.hitData.slice(1))} +
+ ); + } mainContent = (
- {turkStatus[status].message} {currStatus.createBtn && this.renderCreateHitButton()} -
- {currStatus.downloadLink && this.renderDownloadLink()} - {currStatus.fileActions && this.renderHitActions()} +
+ + + + {curHit.response_message ? + () : } + + + +
Status{currStatus.message}
Message{curHit.response_message}
Creation Date{curHit.hit_date}
Creator{curHit.requester_name}
+
+ {currStatus.downloadLink && this.renderDownloadLink(curHit.csv_document_key)} + {currStatus.fileActions && this.renderHitActions(curHit)} +
); @@ -123,10 +203,9 @@ class TurkHit extends Component { > Failed to create HIT. Response message: {hit.create.error.message}
- {this.renderDefinitions()} {mainContent} - + {oldHits}
); } @@ -138,6 +217,7 @@ TurkHit.propTypes = { address: PropTypes.string, building_id: PropTypes.number, hit: turkHitPropTypes, + documents: documentsPropType, }; export default TurkHit; diff --git a/src/components/TurkHit/turkStatus.js b/src/components/TurkHit/turkStatus.js index 5b9478b42459456d04a201b17a543edecd16eec9..edaecf520fca5ef9e39774283be24c6a14caa2f8 100644 --- a/src/components/TurkHit/turkStatus.js +++ b/src/components/TurkHit/turkStatus.js @@ -1,41 +1,48 @@ export default { - Assignable: { + 1: { + statusText: 'Assignable', message: 'HIT has been submitted to mechanical turk and is waiting for worker.', createBtn: false, fileActions: false, downloadLink: false, }, - Unassignable: { + 2: { + statusText: 'Unassignable', message: 'Worker is finding the measurements of the building.', createBtn: false, fileActions: false, downloadLink: false, }, - Reviewable: { + 3: { + statusText: 'Reviewable', message: 'Measurements have been submitted. Check if the file is correct and either approve or reject.', createBtn: false, fileActions: true, downloadLink: true, }, - Accepted: { + 4: { + statusText: 'Accepted', message: 'HIT is completed and has been accepted.', createBtn: false, fileActions: false, downloadLink: true, }, - Rejected: { + 5: { + statusText: 'Rejected', message: 'The HIT has been rejected.', createBtn: true, fileActions: false, downloadLink: true, }, - Disposed: { + 6: { + statusText: 'Disposed', message: 'The HIT has been deleted from mechanical turk. Create a new HIT.', createBtn: true, fileActions: false, downloadLink: false, }, - Expired: { + 7: { + statusText: 'Expired', message: 'The HIT has been expired and must be recreated.', createBtn: true, fileActions: false, diff --git a/src/containers/Dimensions/actions.js b/src/containers/Dimensions/actions.js index 0600cfea77247821da68142fa3a042a5f93e5033..c3ef206215f7eb7ead2d0179116bbfc389987402 100644 --- a/src/containers/Dimensions/actions.js +++ b/src/containers/Dimensions/actions.js @@ -19,10 +19,11 @@ import { * @returns {object} An action object with a type of * LOAD_HIT passing the building_id */ -export function loadHit(buildingId) { +export function loadHit(buildingId, address) { return { type: LOAD_HIT, - payload: buildingId, + buildingId, + address, }; } @@ -33,10 +34,10 @@ export function loadHit(buildingId) { * @returns {object} An action object with a type of * LOAD_HIT_SUCCESS passing the hit status */ -export function hitLoaded(hitStatus) { +export function hitLoaded(hitData) { return { type: LOAD_HIT_SUCCESS, - payload: hitStatus, + payload: hitData, }; } @@ -102,11 +103,12 @@ export function createHitError(error) { * @returns {object} An action object with a type of DECIDE_HIT * passing the decision of the user */ -export function hitDecision(buildingId, decision) { +export function hitDecision(hit, decision, message) { return { type: DECIDE_HIT, - buildingId, + hit, decision, + message, }; } diff --git a/src/containers/Dimensions/index.js b/src/containers/Dimensions/index.js index 137e56ebef6766f08f654d92caa02e9ba108e3a1..cfb68a6a91ddea5f9bb720e15bfa04b1ade8ff7b 100644 --- a/src/containers/Dimensions/index.js +++ b/src/containers/Dimensions/index.js @@ -1,6 +1,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import buildingDetailPropTypes, { completeOverviewPropTypes } from '../../containers/Building/propTypes'; +import documentsPropType from '../Documents/propTypes'; import { loadHit, @@ -8,7 +10,10 @@ import { hitDecision, } from './actions'; -import buildingDetailPropTypes from '../Building/propTypes'; +import { + loadDocuments, +} from '../Documents/actions'; + import turkHitPropTypes from './propTypes'; import './styles.css'; @@ -17,7 +22,22 @@ import TurkHit from '../../components/TurkHit'; class Dimensions extends Component { componentDidMount() { - this.props.loadHit(this.props.buildingId); + this.props.loadHit(this.props.buildingId, this.props.building.address); + } + + componentDidUpdate(prevProps) { + const curHitData = this.props.dimensions.hit.hitData; + const prevHitData = prevProps.dimensions.hit.hitData; + if (curHitData !== prevHitData) { + let docKeys = curHitData.map(val => (val.csv_document_key)); + // Reduce docKeys to remove null entries + docKeys = docKeys.filter(val => ( + val + )); + if (docKeys.length > 0) { + this.props.loadDocuments([], docKeys, 'buildingDimensions'); + } + } } createHitConfirmation = (form) => { @@ -29,14 +49,20 @@ class Dimensions extends Component { } render() { + // TODO: Hardcoded state and country, need to be stored in database return ( -
+
); @@ -45,6 +71,7 @@ class Dimensions extends Component { Dimensions.propTypes = { buildingId: PropTypes.string, + building: completeOverviewPropTypes, buildingDetail: buildingDetailPropTypes, dimensions: PropTypes.shape({ hit: turkHitPropTypes, @@ -52,6 +79,8 @@ Dimensions.propTypes = { loadHit: PropTypes.func, createHit: PropTypes.func, hitDecision: PropTypes.func, + documents: documentsPropType, + loadDocuments: PropTypes.func, }; function mapDispatchToProps(dispatch) { @@ -59,11 +88,12 @@ function mapDispatchToProps(dispatch) { loadHit, createHit, hitDecision, + loadDocuments, }, dispatch); } -function mapStateToProps({ dimensions, buildingDetail }) { - return { dimensions, buildingDetail }; +function mapStateToProps({ dimensions, buildingDetail, documents }) { + return { dimensions, buildingDetail, documents }; } export default connect(mapStateToProps, mapDispatchToProps)(Dimensions); diff --git a/src/containers/Dimensions/propTypes.js b/src/containers/Dimensions/propTypes.js index 8ca01f5449d61c939fb643775d06922f962c13d4..d0d0b824d582dcf7c60a76eab958295bcd2cc363 100644 --- a/src/containers/Dimensions/propTypes.js +++ b/src/containers/Dimensions/propTypes.js @@ -1,17 +1,33 @@ import { PropTypes } from 'react'; -export default PropTypes.shape({ - id: PropTypes.string, - status: PropTypes.string, - loading: PropTypes.boolean, - error: PropTypes.boolean, - boxBuildingList: PropTypes.arrayOf( - PropTypes.shape({ - url_download: PropTypes.string, - }) - ), - create: PropTypes.shape({ - loading: PropTypes.boolean, - error: PropTypes.boolean, +const { shape, arrayOf, string, number, bool, oneOfType, instanceOf } = PropTypes; + +export const hitDataProps = shape({ + status_id: number, + amazon_hit_id: string, + building_id: number, + csv_document_key: string, + db_id: number, + hit_date: string, + requester_name: string, + shapefile_document_key: string, + response_message: string, +}); + +export default shape({ + id: string, + hitData: arrayOf(hitDataProps), + status: number, + loading: bool, + error: oneOfType([ + bool, + instanceOf(Error), + ]), + create: shape({ + loading: bool, + error: oneOfType([ + bool, + instanceOf(Error), + ]), }), }); diff --git a/src/containers/Dimensions/reducer.js b/src/containers/Dimensions/reducer.js index 119117470303cd617e5f7a719abf92796a1f0fbb..7844a6304aafba78041d8b7aa6a90939e5a7fc3a 100644 --- a/src/containers/Dimensions/reducer.js +++ b/src/containers/Dimensions/reducer.js @@ -12,11 +12,11 @@ import { const initState = { hit: { - id: '', - status: '', + id: null, + status: null, + hitData: [], loading: false, error: false, - boxBuildingList: [], create: { loading: false, error: false, @@ -34,22 +34,32 @@ export default function (state = initState, action) { return { hit: { ...state.hit, - status: '', + hitData: [], + id: null, + status: null, loading: true, error: false, }, }; - case LOAD_HIT_SUCCESS: + case LOAD_HIT_SUCCESS: { + let newId = null; + let newStatus = null; + if (action.payload.length > 0) { + newId = action.payload[0].amazon_hit_id; + newStatus = action.payload[0].status_id; + } return { hit: { ...state.hit, - status: action.payload.status, - boxBuildingList: action.payload.box_building_list, + hitData: action.payload, + id: newId, + status: newStatus, loading: false, error: false, }, }; + } case LOAD_HIT_ERROR: return { @@ -74,8 +84,9 @@ export default function (state = initState, action) { case CREATE_HIT_SUCCESS: return { hit: { - id: action.payload.hit_id, - status: action.payload.status, + hitData: [action.payload, ...state.hit.hitData], + id: action.payload.amazon_hit_id, + status: action.payload.status_id, loading: false, error: false, create: { @@ -111,7 +122,11 @@ export default function (state = initState, action) { return { hit: { ...state.hit, - status: action.payload.hit_status, + hitData: [ + action.payload, + ...state.hit.hitData.slice(1), + ], + status: action.payload.status_id, approval: { loading: false, error: false, diff --git a/src/containers/Dimensions/sagas.js b/src/containers/Dimensions/sagas.js index 509d411df60a453bc6a8164072e379e3acfe971e..5dc1e24b64b04ad14d6923e5aa1fa9e3d587c624 100644 --- a/src/containers/Dimensions/sagas.js +++ b/src/containers/Dimensions/sagas.js @@ -23,16 +23,16 @@ import { * @param {object} action Payload attribute with buildingId */ function* loadHit(action) { - const buildingId = action.payload; - const data = yield call(request, `${turkURL}${buildingId}`, { + const buildingId = action.buildingId; + const address = action.address; + const res = yield call(request, `${turkURL}${buildingId}?address=${address}`, { method: 'GET', headers: getHeaders(), }); - - if (!data.err) { - yield put(hitLoaded(data)); + if (!res.err) { + yield put(hitLoaded(res.data)); } else { - yield put(hitLoadingError(data.err)); + yield put(hitLoadingError(res.err)); } } @@ -43,16 +43,16 @@ function* loadHit(action) { */ function* createHit(action) { const turkHitFormData = action.payload; - const data = yield call(request, turkURL, { + const res = yield call(request, turkURL, { method: 'POST', headers: getHeaders(), body: JSON.stringify(turkHitFormData), }); - if (!data.err) { - yield put(createHitSuccess(data)); + if (!res.err) { + yield put(createHitSuccess(res.data)); } else { - yield put(createHitError(data.err)); + yield put(createHitError(res.err)); } } @@ -62,16 +62,19 @@ function* createHit(action) { * @param {number} action 1 or 0 representing approve or reject */ function* approveHit(action) { - const data = yield call(request, `${turkURL}${action.buildingId}`, { + const res = yield call(request, `${turkURL}${action.hit.db_id}`, { method: 'PUT', headers: getHeaders(), - body: JSON.stringify({ accept: action.decision }), + body: JSON.stringify({ + ...action.hit, + approve: action.decision, + response_message: action.message }), }); - if (!data.err) { - yield put(hitDecisionSuccess(data)); + if (!res.err) { + yield put(hitDecisionSuccess(res.data)); } else { - yield put(hitDecisionError(data.err)); + yield put(hitDecisionError(res.err)); } } diff --git a/src/containers/Documents/sagas.js b/src/containers/Documents/sagas.js index 95cd3f92b0999b40905e07939585b790839fa9f6..67b2bc29c25458a81d1499a85081877a9752a8a0 100644 --- a/src/containers/Documents/sagas.js +++ b/src/containers/Documents/sagas.js @@ -18,7 +18,6 @@ import { postToService, } from './actions'; - function* getDocuments(action) { const { documentPaths, documentKeys, fileKey } = action; diff --git a/src/containers/GoogleLogin/index.js b/src/containers/GoogleLogin/index.js index 72e42baf4682ff70ac6251847feccfa0ff2c71c5..5ec211949fdc7854b66c17b09ceaa80c3e0e7b9b 100644 --- a/src/containers/GoogleLogin/index.js +++ b/src/containers/GoogleLogin/index.js @@ -109,9 +109,9 @@ class GoogleLogin extends Component { localStorage.setItem('ed', authResponse.expires_at); if (user) { - localStorage.setItem('name', user.getGivenName()); + localStorage.setItem('name', user.ig); localStorage.setItem('imageUrl', user.getImageUrl()); - this.props.addName(user.getGivenName()); + this.props.addName(user.ig); this.props.addPhoto(user.getImageUrl()); } this.setState({ loggedIn: true });