-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace apply for beta form with separate component to validate project
properties, including tests
- Loading branch information
1 parent
104968b
commit 888b7b4
Showing
5 changed files
with
640 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
import React from 'react'; | ||
import uniq from 'lodash.uniq'; | ||
import apiClient from 'panoptes-client/lib/api-client'; | ||
|
||
const MINIMUM_SUBJECT_COUNT = 100; | ||
const REQUIRED_PAGES = ['Research', 'FAQ']; | ||
|
||
class ApplyForBetaForm extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.attemptApplyForBeta = this.attemptApplyForBeta.bind(this); | ||
this.canApplyForReview = this.canApplyForReview.bind(this); | ||
this.createCheckbox = this.createCheckbox.bind(this); | ||
this.projectHasActiveWorkflows = this.projectHasActiveWorkflows.bind(this); | ||
this.projectHasMinimumActiveSubjects = this.projectHasMinimumActiveSubjects.bind(this); | ||
this.projectHasRequiredContent = this.projectHasRequiredContent.bind(this); | ||
this.projectIsLive = this.projectIsLive.bind(this); | ||
this.projectIsPublic = this.projectIsPublic.bind(this); | ||
this.renderValidationErrors = this.renderValidationErrors.bind(this); | ||
this.testAsyncValidations = this.testAsyncValidations.bind(this); | ||
this.toggleValidation = this.toggleValidation.bind(this); | ||
this.updateValidationsFromProps = this.updateValidationsFromProps.bind(this); | ||
|
||
this.state = { | ||
validations: { | ||
projectIsPublic: this.projectIsPublic(props.project), | ||
projectIsLive: this.projectIsLive(props.project), | ||
projectHasActiveWorkflows: this.projectHasActiveWorkflows(props.workflows), | ||
labPolicyReviewed: false, | ||
bestPracticesReviewed: false, | ||
feedbackReviewed: false, | ||
}, | ||
validationErrors: [], | ||
doingAsyncValidation: false, | ||
}; | ||
} | ||
|
||
attemptApplyForBeta() { | ||
this.testAsyncValidations() | ||
.then(() => { | ||
this.props.applyFn(); | ||
}) | ||
.catch(errors => { | ||
this.setState({ | ||
validationErrors: errors, | ||
}); | ||
}); | ||
} | ||
|
||
projectHasMinimumActiveSubjects(workflows) { | ||
const activeWorkflows = workflows.filter(workflow => workflow.active); | ||
const uniqueSetIDs = uniq(activeWorkflows.map(workflow => workflow.links.subject_sets)); | ||
// Second parameter is an empty object to prevent request caching. | ||
return apiClient.type('subject_sets', {}) | ||
.get(uniqueSetIDs) | ||
.then(sets => { | ||
const subjectCount = sets.reduce((count, set) => count + set.set_member_subjects_count, 0); | ||
return (subjectCount >= MINIMUM_SUBJECT_COUNT) ? true : `The project only has ${subjectCount} of ${MINIMUM_SUBJECT_COUNT} required subjects`; | ||
}); | ||
} | ||
|
||
projectHasRequiredContent(project) { | ||
// Second parameter is an empty object to prevent request caching. | ||
return apiClient.type('projects') | ||
.get(project.id) | ||
.get('pages', {}) | ||
.then(projectPages => { | ||
const missingPages = REQUIRED_PAGES.reduce((accumulator, requiredPage) => { | ||
const pagePresent = projectPages.find(page => requiredPage === page.title); | ||
if (!pagePresent || (pagePresent.content === null || pagePresent.content === '')) | ||
accumulator.push(requiredPage); | ||
return accumulator; | ||
}, []); | ||
return (missingPages.length === 0) ? true : 'The following pages are missing content: ' + missingPages.join(', '); | ||
}); | ||
} | ||
|
||
testAsyncValidations() { | ||
// Resolves to true if everything passes, else rejects with an array of | ||
// error messages. | ||
this.setState({ doingAsyncValidation: true }); | ||
return Promise.all([ | ||
this.projectHasMinimumActiveSubjects(this.props.workflows), | ||
this.projectHasRequiredContent(this.props.project), | ||
]) | ||
.catch(error => console.error('Error requesting project data', error)) | ||
.then(results => { | ||
this.setState({ doingAsyncValidation: false }); | ||
if (results.every(result => typeof result === 'boolean' && result === true)) { | ||
return true; | ||
} | ||
const errors = results.filter(result => typeof result !== 'boolean'); | ||
return Promise.reject(errors); | ||
}) | ||
} | ||
|
||
projectIsPublic(project) { | ||
return project.private === false; | ||
} | ||
|
||
projectIsLive(project) { | ||
return project.live === true; | ||
} | ||
|
||
projectHasActiveWorkflows(workflows) { | ||
return workflows.some(workflow => workflow.active); | ||
} | ||
|
||
toggleValidation(validationName, event) { | ||
const validations = Object.assign({}, this.state.validations); | ||
validations[validationName] = event.target.checked; | ||
this.setState({ validations }); | ||
} | ||
|
||
canApplyForReview() { | ||
const { validations } = this.state; | ||
const values = Object.keys(validations) | ||
.map(key => validations[key]); | ||
return values.every(value => value === true); | ||
} | ||
|
||
createCheckbox(validationName, content, disabled = false) { | ||
// If it's a non-user controlled checkbox, we don't want to trigger anything | ||
// on change, so we use Function.prototype as a noop. | ||
const changeFn = (disabled) ? Function.prototype : this.toggleValidation.bind(this, validationName); | ||
return ( | ||
<label style={{ display: 'block' }}> | ||
<input type="checkbox" | ||
onChange={changeFn} | ||
checked={this.state.validations[validationName] === true} | ||
disabled={disabled} | ||
/> | ||
{content} | ||
</label> | ||
); | ||
} | ||
|
||
componentWillUpdate(nextProps) { | ||
this.updateValidationsFromProps(nextProps); | ||
} | ||
|
||
updateValidationsFromProps(props) { | ||
// We need to do a props comparison, otherwise we get a loop where props | ||
// update state -> updates state repeatedly. | ||
// | ||
// Unfortunately, we have to do it by comparing the new props against the | ||
// current state, instead of using shouldComponentUpdate. This is because | ||
// the project prop passed down is mutable, which breaks the props/nextProps | ||
// comparison used by shouldComponentUpdate. | ||
const validations = Object.assign({}, this.state.validations); | ||
|
||
const newValues = { | ||
projectIsPublic: this.projectIsPublic(props.project), | ||
projectIsLive: this.projectIsLive(props.project), | ||
projectHasActiveWorkflows: this.projectHasActiveWorkflows(props.workflows), | ||
}; | ||
|
||
for (let key in newValues) { | ||
if (validations[key] !== newValues[key]) | ||
validations[key] = newValues[key]; | ||
} | ||
|
||
if (!shallowCompare(validations, this.state.validations)) | ||
this.setState({ validations }); | ||
} | ||
|
||
renderValidationErrors(errors) { | ||
if (errors.length) { | ||
return ( | ||
<div> | ||
<p className="form-help">The following errors need to be fixed:</p> | ||
<ul className="form-help error-messages"> | ||
{errors.map(error => <li key={error}>{error}</li>)} | ||
</ul> | ||
</div> | ||
); | ||
} | ||
return null; | ||
} | ||
|
||
render() { | ||
const applyButtonDisabled = !this.canApplyForReview() || | ||
this.state.doingAsyncValidation; | ||
|
||
return ( | ||
<div> | ||
|
||
{this.createCheckbox('projectIsPublic', <span>Project is public</span>, true)} | ||
|
||
{this.createCheckbox('projectIsLive', <span>Project is live</span>, true)} | ||
|
||
{this.createCheckbox('projectHasActiveWorkflows', <span>Project has at least one active workflow</span>, true)} | ||
|
||
{this.createCheckbox('labPolicyReviewed', <span>I have reviewed the <a href="/lab-policies" target="_blank">policies</a></span>)} | ||
|
||
{this.createCheckbox('bestPracticesReviewed', <span>I have reviewed the <a href="/lab-best-practices" target="_blank">best practices</a></span>)} | ||
|
||
{this.createCheckbox('feedbackReviewed', <span>I have reviewed the sample <a href="https://docs.google.com/a/zooniverse.org/forms/d/1o7yTqpytWWhSOqQhJYiKaeHIaax7xYVUyTOaG3V0xA4/viewform" target="_blank">project review feedback form</a></span>)} | ||
|
||
<p className="form-help">To be eligible for beta review, projects also require:</p> | ||
<ul className="form-help"> | ||
<li>at least {MINIMUM_SUBJECT_COUNT} subjects in active workflows</li> | ||
<li>content on the Research and FAQ pages in the About page</li> | ||
</ul> | ||
<p className="form-help">These will be checked when you click "Apply for review".</p> | ||
|
||
<button | ||
type="button" | ||
className="standard-button" | ||
disabled={applyButtonDisabled} | ||
onClick={this.attemptApplyForBeta} | ||
> | ||
Apply for review | ||
</button> | ||
|
||
{this.renderValidationErrors(this.state.validationErrors)} | ||
|
||
</div> | ||
); | ||
|
||
} | ||
} | ||
|
||
ApplyForBetaForm.defaultProps = { | ||
project: {}, | ||
workflows: [], | ||
} | ||
|
||
export default ApplyForBetaForm; | ||
|
||
// Helper function for comparing objects | ||
const shallowCompare = (a, b) => { | ||
for (let key in a) { | ||
if (!(key in b) || a[key] !== b[key]) | ||
return false; | ||
} | ||
for (let key in b) { | ||
if(!(key in a) || a[key] !== b[key]) | ||
return false; | ||
} | ||
return true; | ||
} |
Oops, something went wrong.