Skip to content

Commit

Permalink
Merge pull request #2051 from zooniverse/flexible-survey
Browse files Browse the repository at this point in the history
Flexible survey
  • Loading branch information
aweiksnar committed Dec 22, 2015
2 parents 5f6d608 + 13e5e9f commit d13ced3
Show file tree
Hide file tree
Showing 13 changed files with 990 additions and 0 deletions.
14 changes: 14 additions & 0 deletions app/classifier/mock-data.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ workflow = apiClient.type('workflows').create
confusionsOrder: []
confusions: {}

questionsMap:
ar: ['ho', 'be']
to: ['ho', 'be', 'in', 'bt']
questionsOrder: ['ho', 'be', 'in', 'hr']
questions:
ho:
Expand Down Expand Up @@ -232,6 +235,17 @@ workflow = apiClient.type('workflows').create
answers:
y:
label: 'Present'
bt:
required: true
multiple: false
label: 'Are tortoises awesome?'
answersOrder: ['y','Y']
answers:
y:
label: 'yes'
Y:
label: 'HECK YES'

next: 'init'

subject = apiClient.type('subjects').create
Expand Down
35 changes: 35 additions & 0 deletions app/classifier/tasks/flexible-survey/annotation-view.cjsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
React = require 'react'

module.exports = React.createClass
displayName: 'SurveyAnnotationView'

getDefaultProperties: ->
task: null
classification: null
annotation: null

render: ->
<div>
{for identification, i in @props.annotation.value
identification._key ?= Math.random()

answersByQuestion = @props.task.questionsOrder.map (questionID) =>
if questionID of identification.answers
answerLabels = [].concat(identification.answers[questionID]).map (answerID) =>
@props.task.questions[questionID].answers[answerID].label
answerLabels.join ', '
answersList = answersByQuestion.filter(Boolean).join '; '

<span key={identification._key}>
<span className="survey-identification-proxy" title={answersList}>
{@props.task.choices[identification.choice].label}
{' '}
<button type="button" className="survey-identification-remove" title="Remove" onClick={@handleRemove.bind this, i}>&times;</button>
</span>
{' '}
</span>}
</div>

handleRemove: (index) ->
@props.annotation.value.splice index, 1
@props.classification.update 'annotations'
153 changes: 153 additions & 0 deletions app/classifier/tasks/flexible-survey/choice.cjsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
React = require 'react'
TriggeredModalForm = require 'modal-form/triggered'
{Markdown} = require 'markdownz'
Utility = require './utility'

ImageFlipper = React.createClass
displayName: 'ImageFlipper'

getDefaultProps: ->
images: []

getInitialState: ->
frame: 0

PRELOAD_STYLE:
height: 0
overflow: 'hidden'
position: 'fixed'
right: 0
width: 0

render: ->
<span className="survey-task-image-flipper">
{@renderPreload()}
<img src={@props.images[@state.frame]} className="survey-task-image-flipper-image" />
<span className="survey-task-image-flipper-pips">
{unless @props.images.length is 1
for index in [0...@props.images.length]
<span key={@props.images[index]}>
<button type="button" className="survey-task-image-flipper-pip" disabled={index is @state.frame} onClick={@handleFrameChange.bind this, index}>{index + 1}</button>
{' '}
</span>}
</span>
</span>

renderPreload: ->
<div style={@PRELOAD_STYLE}>
{for image in @props.images
<img src={image} key={image} />}
</div>

handleFrameChange: (frame) ->
@setState {frame}

module.exports = React.createClass
displayName: 'FlexibleChoice'

getDefaultProps: ->
task: null
choiceID: ''
onSwitch: Function.prototype
onConfirm: Function.prototype
onCancel: Function.prototype

getInitialState: ->
answers: {}

allFilledIn: ->
for questionID in Utility.getQuestionIDs(@props.task, @props.choiceID)
question = @props.task.questions[questionID]
if question.required
answer = @state.answers[questionID]
if (not answer?) or (question.multiple and answer.length is 0)
return false
true

render: ->
choice = @props.task.choices[@props.choiceID]
<div className="survey-task-choice">
{unless choice.images.length is 0
<ImageFlipper images={@props.task.images[filename] for filename in choice.images} />}
<div className="survey-task-choice-content">
<div className="survey-task-choice-label">{choice.label}</div>
<div className="survey-task-choice-description">{choice.description}</div>

{unless choice.confusionsOrder.length is 0
<div className="survey-task-choice-confusions">
Often confused with
{' '}
{for otherChoiceID in choice.confusionsOrder
otherChoice = @props.task.choices[otherChoiceID]
<span key={otherChoiceID}>
<TriggeredModalForm className="survey-task-confusions-modal" trigger={
<span className="survey-task-choice-confusion">
{otherChoice.label}
</span>
} style={maxWidth: '60ch'}>
<ImageFlipper images={@props.task.images[filename] for filename in otherChoice.images} />
<Markdown content={choice.confusions[otherChoiceID]} />
<div className="survey-task-choice-confusion-buttons" style={textAlign: 'center'}>
<button type="submit" className="major-button identfiy">Dismiss</button>
{' '}
<button type="button" className="standard-button cancel" onClick={@props.onSwitch.bind null, otherChoiceID}>I think it’s this</button>
</div>
</TriggeredModalForm>
{' '}
</span>}
</div>}

<hr />

{unless choice.noQuestions
for questionID in Utility.getQuestionIDs(@props.task, @props.choiceID)
question = @props.task.questions[questionID]
inputType = if question.multiple
'checkbox'
else
'radio'
<div key={questionID} className="survey-task-choice-question" data-multiple={question.multiple || null}>
<div className="survey-task-choice-question-label">{question.label}</div>
{for answerID in question.answersOrder
answer = question.answers[answerID]
isChecked = if question.multiple
answerID in (@state.answers[questionID] ? [])
else
answerID is @state.answers[questionID]
<span key={answerID}>
<label className="survey-task-choice-answer" data-checked={isChecked || null}>
<input type={inputType} checked={isChecked} onChange={@handleAnswer.bind this, questionID, answerID} />
{answer.label}
</label>
{' '}
</span>}
</div>}

{unless choice.noQuestions or Utility.getQuestionIDs(@props.task, @props.choiceID).length is 0
<hr />}
</div>
<div style={textAlign: 'center'}>
<button type="button" className="minor-button" onClick={@props.onCancel}>Cancel</button>
{' '}
<button type="button" className="standard-button" disabled={not @allFilledIn()} onClick={@handleIdentification}>
<strong>Identify</strong>
</button>
</div>
</div>

handleAnswer: (questionID, answerID, e) ->
if @props.task.questions[questionID].multiple
@state.answers[questionID] ?= []
if e.target.checked
@state.answers[questionID].push answerID
else
@state.answers[questionID].splice @state.answers[questionID].indexOf(answerID), 1
else
@state.answers[questionID] = if e.target.checked
answerID
else
null
@setState answers: @state.answers

handleIdentification: ->
@props.onConfirm @props.choiceID, @state.answers
109 changes: 109 additions & 0 deletions app/classifier/tasks/flexible-survey/chooser.cjsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
React = require 'react'
TriggeredModalForm = require 'modal-form/triggered'

THUMBNAIL_BREAKPOINTS = [Infinity, 40, 20, 10, 5, 0]

module.exports = React.createClass
displayName: 'Chooser'

getDefaultProps: ->
task: null
filters: {}
onFilter: Function.prototype
onChoose: Function.prototype

getFilteredChoices: ->
for choiceID in @props.task.choicesOrder
choice = @props.task.choices[choiceID]
rejected = false
for characteristicID, valueID of @props.filters
if valueID not in choice.characteristics[characteristicID]
rejected = true
break
if rejected
continue
else
choiceID

render: ->
filteredChoices = @getFilteredChoices()

for point in THUMBNAIL_BREAKPOINTS
if filteredChoices.length <= point
breakpoint = point

<div className="survey-task-chooser">
<div className="survey-task-chooser-characteristics">
{for characteristicID in @props.task.characteristicsOrder
characteristic = @props.task.characteristics[characteristicID]
selectedValue = characteristic.values[@props.filters[characteristicID]]
hasBeenAutoFocused = false

<TriggeredModalForm key={characteristicID} ref="#{characteristicID}-dropdown" className="survey-task-chooser-characteristic-menu" trigger={
<span className="survey-task-chooser-characteristic" data-is-active={selectedValue? || null}>
<span className="survey-task-chooser-characteristic-label">{selectedValue?.label ? characteristic.label}</span>
</span>
}>
<div className="survey-task-chooser-characteristic-menu-container">
{for valueID in characteristic.valuesOrder
value = characteristic.values[valueID]

disabled = valueID is @props.filters[characteristicID]
autoFocus = not disabled and not hasBeenAutoFocused
selected = valueID is @props.filters[characteristicID]

if autoFocus
hasBeenAutoFocused = true

<button key={valueID} type="submit" title={value.label} className="survey-task-chooser-characteristic-value" disabled={disabled} data-selected={selected} autoFocus={autoFocus} onClick={@handleFilter.bind this, characteristicID, valueID}>
{if value.image?
<img src={@props.task.images[value.image]} alt={value.label} className="survey-task-chooser-characteristic-value-icon" />}
</button>}

<button type="submit" className="survey-task-chooser-characteristic-clear-button" disabled={characteristicID not of @props.filters} autoFocus={not hasBeenAutoFocused} onClick={@handleFilter.bind this, characteristicID, undefined}>
Clear
</button>
</div>
<div className="survey-task-chooser-characteristic-value-label">
{label = ""
for valueID in characteristic.valuesOrder
value = characteristic.values[valueID]

if valueID is @props.filters[characteristicID]
label = value.label
if label then label else "Make a selection"
}</div>
</TriggeredModalForm>}
</div>

<div className="survey-task-chooser-choices" data-breakpoint={breakpoint}>
{if filteredChoices.length is 0
<div>
<em>No matches.</em>
</div>
else
for choiceID, i in filteredChoices
choice = @props.task.choices[choiceID]
<button key={choiceID + i} type="button" className="survey-task-chooser-choice" onClick={@props.onChoose.bind null, choiceID}>
{unless choice.images.length is 0
<span className="survey-task-chooser-choice-thumbnail-container">
<img src={@props.task.images[choice.images[0]]} alt={choice.label} className="survey-task-chooser-choice-thumbnail" />
</span>}
<span className="survey-task-chooser-choice-label">{choice.label}</span>
</button>}
</div>
<div style={textAlign: 'center'}>
Showing {filteredChoices.length} of {@props.task.choicesOrder.length}.
&ensp;
<button type="button" className="survey-task-chooser-characteristic-clear-button" disabled={Object.keys(@props.filters).length is 0} onClick={@handleClearFilters}>
<i className="fa fa-ban"></i> Clear filters
</button>
</div>
</div>

handleFilter: (characteristicID, valueID) ->
@props.onFilter characteristicID, valueID

handleClearFilters: ->
for characteristicID in @props.task.characteristicsOrder
@props.onFilter characteristicID, undefined
Loading

0 comments on commit d13ced3

Please sign in to comment.