obojobo-sections-assessment
Advanced tools
Comparing version 12.0.0 to 12.1.0-alpha.0
@@ -8,6 +8,2 @@ import React from 'react' | ||
const onCancel = () => { | ||
ModalUtil.hide() | ||
} | ||
const onSubmit = submitProp => { | ||
@@ -30,3 +26,3 @@ ModalUtil.hide() | ||
value: 'Resume assessment', | ||
onClick: onCancel, | ||
onClick: props.onCancel, | ||
default: true | ||
@@ -33,0 +29,0 @@ } |
@@ -5,5 +5,4 @@ import React from 'react' | ||
const { Dialog } = Common.components.modal | ||
const { ModalUtil } = Common.util | ||
const UpdatedModuleDialog = ({ onConfirm }) => ( | ||
const UpdatedModuleDialog = ({ onClose, onRestart }) => ( | ||
<Dialog | ||
@@ -18,7 +17,7 @@ preventEsc | ||
default: true, | ||
onClick: ModalUtil.hide | ||
onClick: onClose | ||
}, | ||
{ | ||
value: 'Restart', | ||
onClick: onConfirm | ||
onClick: onRestart | ||
} | ||
@@ -25,0 +24,0 @@ ]} |
@@ -20,2 +20,3 @@ import Common from 'obojobo-document-engine/src/scripts/common' | ||
) | ||
NavUtil.setContext(`assessmentReview:${lastAttempt.id}`) | ||
@@ -44,3 +45,3 @@ } | ||
const machineDateString = formatDate(date, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx") | ||
const ariaDateString = formatDate(date, "MMMM Do yyyy 'at' h:mmaaaa") | ||
const ariaDateString = formatDate(date, "MMMM do yyyy 'at' h:mmaaaa") | ||
const numCorrect = AssessmentUtil.getNumCorrect(attempt.result.questionScores) | ||
@@ -47,0 +48,0 @@ const numPossibleCorrect = AssessmentUtil.getNumPossibleCorrect(attempt.result.questionScores) |
@@ -7,4 +7,2 @@ import Common from 'Common' | ||
import Viewer from 'Viewer' | ||
import AssessmentApi from 'obojobo-document-engine/src/scripts/viewer/util/assessment-api' | ||
import injectKatexIfNeeded from 'obojobo-document-engine/src/scripts/common/util/inject-katex-if-needed' | ||
@@ -15,2 +13,3 @@ const { OboModel } = Common.models | ||
const { Dispatcher } = Common.flux | ||
const { Spinner } = Common.components | ||
@@ -21,7 +20,2 @@ class AssessmentPostTest extends React.Component { | ||
this.state = { | ||
attempts: null, | ||
isFetching: true | ||
} | ||
this.h1Ref = React.createRef() | ||
@@ -31,3 +25,3 @@ this.ltiStatusRef = React.createRef() | ||
this.onClickResendScore = this.onClickResendScore.bind(this) | ||
this.renderFullReview = this.renderFullReview.bind(this) | ||
this.renderReview = this.renderReview.bind(this) | ||
} | ||
@@ -37,3 +31,2 @@ | ||
Dispatcher.on(FOCUS_ON_ASSESSMENT_CONTENT, this.boundFocusOnContent) | ||
this.fetchAttemptReviews() // WARNING - ASYNC | ||
} | ||
@@ -46,33 +39,11 @@ | ||
focusOnContent() { | ||
focus(this.h1Ref) | ||
} | ||
// If the focus is not at the default (document.body) then we assume it's on a modal | ||
// and we don't want to steal focus, so we do nothing. However if the focus is at | ||
// the default location we move focus to the top of this component page. | ||
if (document.activeElement === document.body) { | ||
focus(this.h1Ref) | ||
return true | ||
} | ||
// WARNING - ASYNC | ||
fetchAttemptReviews() { | ||
const attemptIds = [] | ||
const attempts = AssessmentUtil.getAllAttempts( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
attempts.forEach(attempt => { | ||
attemptIds.push(attempt.id) | ||
}) | ||
return AssessmentApi.reviewAttempt(attemptIds).then(result => { | ||
attempts.forEach(attempt => { | ||
attempt.state.questionModels = result[attempt.id] | ||
}) | ||
// Some of the questions may contain latex. Need to | ||
// make sure window.katex is defined before trying | ||
// to render those questions | ||
injectKatexIfNeeded({ value: attempts }).finally(() => { | ||
this.setState({ | ||
attempts, | ||
isFetching: false | ||
}) | ||
}) | ||
}) | ||
return false | ||
} | ||
@@ -84,22 +55,14 @@ | ||
isFullReviewAvailable(model, assessmentState) { | ||
switch (model.modelState.review) { | ||
case 'always': | ||
return true | ||
case 'never': | ||
return false | ||
case 'no-attempts-remaining': | ||
return !AssessmentUtil.hasAttemptsRemaining(assessmentState, model) | ||
} | ||
} | ||
renderReview() { | ||
const showFullReview = AssessmentUtil.isFullReviewAvailableForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
renderFullReview() { | ||
const showFullReview = this.isFullReviewAvailable( | ||
this.props.model, | ||
this.props.moduleData.assessmentState | ||
const attempts = AssessmentUtil.getAllAttempts( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
return ( | ||
<FullReview {...this.props} showFullReview={showFullReview} attempts={this.state.attempts} /> | ||
) | ||
return <FullReview {...this.props} showFullReview={showFullReview} attempts={attempts} /> | ||
} | ||
@@ -153,2 +116,7 @@ | ||
const isAttemptHistoryLoadedForModel = AssessmentUtil.isAttemptHistoryLoadedForModel( | ||
props.moduleData.assessmentState, | ||
props.model | ||
) | ||
const assessmentScore = AssessmentUtil.getAssessmentScoreForModel( | ||
@@ -168,21 +136,27 @@ props.moduleData.assessmentState, | ||
<div className="score unlock"> | ||
<div className="overview"> | ||
<h1 ref={this.h1Ref} tabIndex="-1"> | ||
{assessmentLabel} Overview | ||
</h1> | ||
{this.renderRecordedScore(assessmentScore, props)} | ||
<LTIStatus | ||
ref={this.ltiStatusRef} | ||
ltiState={ltiState} | ||
isPreviewing={props.moduleData.isPreviewing} | ||
externalSystemLabel={props.moduleData.lti.outcomeServiceHostname} | ||
onClickResendScore={this.onClickResendScore} | ||
assessmentScore={assessmentScore} | ||
/> | ||
<div className="score-actions-page pad">{this.renderScoreActionsPage(props)}</div> | ||
</div> | ||
<div className="attempt-history"> | ||
<h1>Attempt History:</h1> | ||
{this.state.isFetching ? <span>Loading...</span> : this.renderFullReview()} | ||
</div> | ||
{isAttemptHistoryLoadedForModel ? ( | ||
<React.Fragment> | ||
<div className="overview"> | ||
<h1 ref={this.h1Ref} tabIndex="-1"> | ||
{assessmentLabel} Overview | ||
</h1> | ||
{this.renderRecordedScore(assessmentScore, props)} | ||
<LTIStatus | ||
ref={this.ltiStatusRef} | ||
ltiState={ltiState} | ||
isPreviewing={props.moduleData.isPreviewing} | ||
externalSystemLabel={props.moduleData.lti.outcomeServiceHostname} | ||
onClickResendScore={this.onClickResendScore} | ||
assessmentScore={assessmentScore} | ||
/> | ||
<div className="score-actions-page pad">{this.renderScoreActionsPage(props)}</div> | ||
</div> | ||
<div className="attempt-history"> | ||
<h1>Attempt History:</h1> | ||
{this.renderReview()} | ||
</div> | ||
</React.Fragment> | ||
) : ( | ||
<Spinner /> | ||
)} | ||
</div> | ||
@@ -189,0 +163,0 @@ ) |
@@ -7,2 +7,3 @@ /* eslint-disable no-undefined */ | ||
const content = Object.assign({}, node.content) | ||
delete content.showModProperties | ||
@@ -9,0 +10,0 @@ if (content.type !== 'pass-fail') return '' |
@@ -5,11 +5,11 @@ import './editor-component.scss' | ||
import { ReactEditor } from 'slate-react' | ||
import { Transforms, Editor } from 'slate' | ||
import { Transforms } from 'slate' | ||
import Common from 'obojobo-document-engine/src/scripts/common' | ||
import isOrNot from 'obojobo-document-engine/src/scripts/common/util/isornot' | ||
import EditorUtil from 'obojobo-document-engine/src/scripts/oboeditor/util/editor-util' | ||
import withSlateWrapper from 'obojobo-document-engine/src/scripts/oboeditor/components/node/with-slate-wrapper' | ||
import ModProperties from './mod-properties' | ||
import RubricModal from './rubric-modal' | ||
const getParsedRange = Common.util.RangeParsing.getParsedRange | ||
const { Button } = Common.components | ||
const { ModalUtil } = Common.util | ||
const { OboModel } = Common.models | ||
@@ -20,90 +20,42 @@ class Rubric extends React.Component { | ||
const content = this.props.element.content | ||
this.state = { | ||
passingAttemptScore: | ||
typeof content.passingAttemptScore !== 'undefined' ? content.passingAttemptScore : 100, | ||
passedResult: typeof content.passedResult !== 'undefined' ? content.passedResult : 100, | ||
failedResult: typeof content.failedResult !== 'undefined' ? content.failedResult : 0, | ||
unableToPassResult: | ||
typeof content.unableToPassResult !== 'undefined' ? content.unableToPassResult : null | ||
} | ||
this.selfRef = React.createRef() | ||
this.unfreezeEditor = this.unfreezeEditor.bind(this) | ||
this.freezeEditor = this.freezeEditor.bind(this) | ||
this.changeRubricType = this.changeRubricType.bind(this) | ||
this.showModModal = this.showModModal.bind(this) | ||
this.changeMods = this.changeMods.bind(this) | ||
this.onChangeState = this.onChangeState.bind(this) | ||
this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this) | ||
this.updateNodeFromState = this.updateNodeFromState.bind(this) | ||
this.passedType = this.changeScoreType.bind(this, 'passedType') | ||
this.failedType = this.changeScoreType.bind(this, 'failedType') | ||
this.unableToPassType = this.changeScoreType.bind(this, 'unableToPassType') | ||
this.openAssessmentRubricModal = this.openAssessmentRubricModal.bind(this) | ||
this.changeRubricProperties = this.changeRubricProperties.bind(this) | ||
this.onCloseRubricModal = this.onCloseRubricModal.bind(this) | ||
} | ||
onDocumentMouseDown(event) { | ||
if (!this.selfRef.current.contains(event.target)) { | ||
this.updateNodeFromState() | ||
} | ||
changeRubricProperties(content) { | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
Transforms.setNodes(this.props.editor, { content: { ...content } }, { at: path }) | ||
this.onCloseRubricModal() | ||
} | ||
componentDidMount() { | ||
document.addEventListener('mousedown', this.onDocumentMouseDown) | ||
onCloseRubricModal() { | ||
ModalUtil.hide() | ||
this.unfreezeEditor() | ||
} | ||
componentWillUnmount() { | ||
document.removeEventListener('mousedown', this.onDocumentMouseDown) | ||
freezeEditor() { | ||
this.props.editor.toggleEditable(false) | ||
} | ||
changeRubricType(event) { | ||
const type = event.target.value | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
Transforms.setNodes( | ||
this.props.editor, | ||
{ content: { ...this.props.element.content, type } }, | ||
{ at: path } | ||
) | ||
unfreezeEditor() { | ||
this.props.editor.toggleEditable(true) | ||
} | ||
updateNodeFromState() { | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
Transforms.setNodes( | ||
this.props.editor, | ||
{ content: { ...this.props.element.content, ...this.state } }, | ||
{ at: path } | ||
) | ||
} | ||
openAssessmentRubricModal(event) { | ||
event.preventDefault() | ||
event.stopPropagation() | ||
onChangeState(event) { | ||
const { name, value } = event.target | ||
this.freezeEditor() | ||
this.setState({ ...this.state, [name]: value }) | ||
} | ||
const currentAssessmentId = EditorUtil.getCurrentAssessmentId(OboModel.models) | ||
changeScoreType(typeName, event) { | ||
const content = {} | ||
content[typeName] = event.target.value | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
Transforms.setNodes( | ||
this.props.editor, | ||
{ content: { ...this.props.element.content, ...content } }, | ||
{ at: path } | ||
) | ||
} | ||
showModModal() { | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
const [parent] = Editor.parent(this.props.editor, path) | ||
ModalUtil.show( | ||
<ModProperties | ||
mods={this.props.element.content.mods} | ||
attempts={parent.content.attempts} | ||
onConfirm={this.changeMods} | ||
<RubricModal | ||
{...this.props} | ||
onConfirm={this.changeRubricProperties} | ||
onCancel={this.onCloseRubricModal} | ||
model={OboModel.models[currentAssessmentId]} | ||
/> | ||
@@ -113,221 +65,9 @@ ) | ||
changeMods(content) { | ||
ModalUtil.hide() | ||
const path = ReactEditor.findPath(this.props.editor, this.props.element) | ||
Transforms.setNodes( | ||
this.props.editor, | ||
{ content: { ...this.props.element.content, mods: content.mods } }, | ||
{ at: path } | ||
) | ||
} | ||
printRange(range) { | ||
if (range.min === range.max) { | ||
const attempt = range.min === '$last_attempt' ? 'the last attempt' : 'attempt ' + range.min | ||
return <span> If a student passes on {attempt} </span> | ||
} | ||
if (range.min === '$last_attempt') range.min = 'the last attempt' | ||
if (range.max === '$last_attempt') range.max = 'the last attempt' | ||
return ( | ||
<span> | ||
{' '} | ||
If a student passes on attempt {range.min} through {range.max}{' '} | ||
</span> | ||
) | ||
} | ||
freezeEditor() { | ||
this.props.editor.toggleEditable(false) | ||
} | ||
unfreezeEditor() { | ||
this.props.editor.toggleEditable(true) | ||
} | ||
render() { | ||
const content = this.props.element.content | ||
const className = 'rubric pad ' + 'is-type-' + content.type | ||
const stopPropagation = event => event.stopPropagation() | ||
return ( | ||
<div className={className} contentEditable={false} ref={this.selfRef}> | ||
<h2 contentEditable={false}>Assessment Scoring</h2> | ||
<p> | ||
The recorded score for this module is the highest assessment score, and will be sent to | ||
any connected gradebook.{' '} | ||
</p> | ||
<fieldset className="assessment-score"> | ||
<legend>How do you want to determine the recorded score?</legend> | ||
<label> | ||
<input | ||
type="radio" | ||
name="score-type" | ||
value="highest" | ||
checked={content.type === 'highest'} | ||
onChange={this.changeRubricType} | ||
onClick={stopPropagation} | ||
/> | ||
Use the highest attempt score. | ||
</label> | ||
<label> | ||
<input | ||
type="radio" | ||
name="score-type" | ||
value="pass-fail" | ||
checked={content.type === 'pass-fail'} | ||
onChange={this.changeRubricType} | ||
onClick={stopPropagation} | ||
/> | ||
Calculate based on a threshold (pass/fail)... | ||
</label> | ||
</fieldset> | ||
<fieldset className="pass-fail"> | ||
<legend>Pass & Fail Rules</legend> | ||
<p> | ||
In this mode, students must achieve a certain threshold on an attempt to pass. The | ||
assessment score for each attempt will be set based on whether the student passes or | ||
fails, and the highest of these assessment scores will be used as the recorded score. | ||
</p> | ||
<div> | ||
<label> | ||
To <b>pass</b>, students must achieve an attempt score of at least | ||
<input | ||
type="number" | ||
min="0" | ||
max="100" | ||
name="passingAttemptScore" | ||
value={this.state.passingAttemptScore} | ||
onChange={this.onChangeState} | ||
onClick={stopPropagation} | ||
onFocus={this.freezeEditor} | ||
onBlur={this.unfreezeEditor} | ||
/> | ||
% | ||
</label> | ||
</div> | ||
<div> | ||
<label> | ||
When <b>passing</b>, set the assessment score to | ||
<select | ||
value={content.passedType} | ||
onChange={this.passedType} | ||
onClick={stopPropagation} | ||
> | ||
<option value="$attempt_score">The attempt score</option> | ||
<option value="set-value">Specified value</option> | ||
</select> | ||
</label> | ||
<label className={isOrNot(content.passedType === 'set-value', 'enabled')}> | ||
<input | ||
type="number" | ||
min="0" | ||
max="100" | ||
name="passedResult" | ||
value={this.state.passedResult} | ||
onClick={stopPropagation} | ||
onChange={this.onChangeState} | ||
disabled={content.passedType !== 'set-value'} | ||
onFocus={this.freezeEditor} | ||
onBlur={this.unfreezeEditor} | ||
/> | ||
% | ||
</label> | ||
</div> | ||
<div> | ||
<label> | ||
When <b>failing</b>, | ||
<select | ||
value={content.failedType} | ||
onChange={this.failedType} | ||
onClick={stopPropagation} | ||
> | ||
<option value="$attempt_score"> | ||
Set the assessment score to the attempt score | ||
</option> | ||
<option value="no-score"> | ||
Don't set the score (no score will be sent to the gradebook) | ||
</option> | ||
<option value="set-value">Set the assessment score to specified value</option> | ||
</select> | ||
</label> | ||
<label className={isOrNot(content.failedType === 'set-value', 'enabled')}> | ||
<input | ||
type="number" | ||
min="0" | ||
max="100" | ||
name="failedResult" | ||
value={this.state.failedResult} | ||
onClick={stopPropagation} | ||
onChange={this.onChangeState} | ||
disabled={content.failedType !== 'set-value'} | ||
onFocus={this.freezeEditor} | ||
onBlur={this.unfreezeEditor} | ||
/> | ||
% | ||
</label> | ||
</div> | ||
<div> | ||
<label> | ||
And if the student is <b>out of attempts and still did not pass</b>, | ||
<select | ||
value={content.unableToPassType} | ||
onChange={this.unableToPassType} | ||
onClick={stopPropagation} | ||
> | ||
<option value="no-value"> | ||
Don't do anything, the failing rule will still apply | ||
</option> | ||
<option value="$highest_attempt_score"> | ||
Set the assessment score to the highest attempt score | ||
</option> | ||
<option value="no-score"> | ||
Don't set assessment the score (no score will be sent to the gradebook) | ||
</option> | ||
<option value="set-value">Set the assessment score to specified value</option> | ||
</select> | ||
</label> | ||
<label className={isOrNot(content.unableToPassType === 'set-value', 'enabled')}> | ||
<input | ||
type="number" | ||
min="0" | ||
max="100" | ||
name="unableToPassResult" | ||
value={this.state.unableToPassResult || 0} | ||
onClick={stopPropagation} | ||
onChange={this.onChangeState} | ||
disabled={content.unableToPassType !== 'set-value'} | ||
onFocus={this.freezeEditor} | ||
onBlur={this.unfreezeEditor} | ||
/> | ||
% | ||
</label> | ||
</div> | ||
</fieldset> | ||
<div className="mods"> | ||
<div className="title">Extra Credit & Penalties</div> | ||
<Button onClick={this.showModModal}>Edit...</Button> | ||
<ul> | ||
{content.mods.map((mod, index) => { | ||
const range = getParsedRange(mod.attemptCondition + '') | ||
return ( | ||
<li key={index}> | ||
{mod.reward < 0 ? ( | ||
<b> | ||
<span className="deduct">Deduct</span> {Math.abs(mod.reward)}% | ||
</b> | ||
) : ( | ||
<b> | ||
<span className="reward">Add</span> {mod.reward}% | ||
</b> | ||
)} | ||
{this.printRange(range)} | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
</div> | ||
<span className="invisibleText">{this.props.children}</span> | ||
<Button onClick={this.openAssessmentRubricModal}>Edit Assessment Rubric</Button> | ||
</div> | ||
@@ -334,0 +74,0 @@ ) |
@@ -6,3 +6,2 @@ import './mod-properties.scss' | ||
const { SimpleDialog } = Common.components.modal | ||
const { Button } = Common.components | ||
@@ -35,7 +34,12 @@ const { Slider } = Common.components.slider | ||
const attemptCondition = '[' + lowerVal + ',' + upperVal + ']' | ||
this.setState(prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
@@ -45,7 +49,12 @@ | ||
const reward = event.target.value | ||
this.setState(prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { reward }) : mod | ||
) | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { reward }) : mod | ||
) | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
@@ -57,7 +66,12 @@ | ||
const attemptCondition = '[' + lower + ',' + range.max + ']' | ||
this.setState(prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
@@ -69,21 +83,36 @@ | ||
const attemptCondition = '[' + range.min + ',' + upper + ']' | ||
this.setState(prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: prevState.mods.map((mod, listIndex) => | ||
index === listIndex ? Object.assign(mod, { attemptCondition }) : mod | ||
) | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
onAddMod() { | ||
this.setState(prevState => ({ | ||
mods: [...prevState.mods, { reward: 0, attemptCondition: '[1,$last_attempt]' }] | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: [...prevState.mods, { reward: 0, attemptCondition: '[1,$last_attempt]' }] | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
deleteMod(index) { | ||
this.setState(prevState => ({ | ||
mods: prevState.mods | ||
.map((mod, listIndex) => (index === listIndex ? null : mod)) | ||
.filter(Boolean) | ||
})) | ||
this.setState( | ||
prevState => ({ | ||
mods: prevState.mods | ||
.map((mod, listIndex) => (index === listIndex ? null : mod)) | ||
.filter(Boolean) | ||
}), | ||
() => { | ||
this.props.updateModProperties(this.state.mods) | ||
} | ||
) | ||
} | ||
@@ -93,75 +122,68 @@ | ||
return ( | ||
<SimpleDialog | ||
ok | ||
title="Extra Credit & Penalties" | ||
onConfirm={() => this.props.onConfirm(this.state)} | ||
focusOnFirstElement={this.focusOnFirstElement} | ||
> | ||
<div className="obojobo-draft--sections--assessment--mod-properties"> | ||
<p className="info" ref={this.inputRef} tabIndex={-1}> | ||
You can add or deduct percentage points from a student's assessment score based on | ||
which attempt they achived a passing score. (The final assessment score is still limited | ||
to a maximum of 100%) | ||
</p> | ||
<div className="mod-box"> | ||
{this.state.mods.map((mod, index) => { | ||
const range = getParsedRange(mod.attemptCondition) | ||
// If there are unlimited attempts, limit the mods to the first 20 attempts | ||
// Otherwise, add one to the number of attempts to make a space for $last_attempt | ||
const upperRange = | ||
this.props.attempts === 'unlimited' ? 20 : parseInt(this.props.attempts, 10) + 1 | ||
<div className="obojobo-draft--sections--assessment--mod-properties"> | ||
<p className="info" ref={this.inputRef} tabIndex={-1}> | ||
You can add or deduct percentage points from a student's assessment score based on | ||
which attempt they achived a passing score. (The final assessment score is still limited | ||
to a maximum of 100%) | ||
</p> | ||
<div className="mod-box"> | ||
{this.state.mods.map((mod, index) => { | ||
const range = getParsedRange(mod.attemptCondition) | ||
// If there are unlimited attempts, limit the mods to the first 20 attempts | ||
// Otherwise, add one to the number of attempts to make a space for $last_attempt | ||
const upperRange = | ||
this.props.attempts === 'unlimited' ? 20 : parseInt(this.props.attempts, 10) + 1 | ||
// safely wrap string values like $last_attempt as the highest value | ||
const lower = parseInt(range.min, 10) | ||
const lowerVal = isNaN(lower) ? upperRange : lower | ||
const upper = parseInt(range.max, 10) | ||
const upperVal = isNaN(upper) ? upperRange : upper | ||
// Safely wrap string values like $last_attempt as the highest value | ||
const lower = parseInt(range.min, 10) | ||
const lowerVal = isNaN(lower) ? upperRange : lower | ||
const upper = parseInt(range.max, 10) | ||
const upperVal = isNaN(upper) ? upperRange : upper | ||
return ( | ||
<div key={index} className="mod"> | ||
<Button className="delete-button" onClick={this.deleteMod.bind(this, index)}> | ||
× | ||
</Button> | ||
<label>When passing on attempt</label> | ||
<div className="slider-container"> | ||
<Slider | ||
domain={[1, upperRange]} | ||
values={[lowerVal, upperVal]} | ||
step={1} | ||
onChange={this.onChangeSlider.bind(this, index)} | ||
/> | ||
</div> | ||
<div className="slider-inputs"> | ||
<input | ||
type="text" | ||
value={range.min} | ||
className="min-input" | ||
onChange={this.onChangeLower.bind(this, index)} | ||
/> | ||
through | ||
<input | ||
type="text" | ||
value={range.max} | ||
className="max-input" | ||
onChange={this.onChangeUpper.bind(this, index)} | ||
/> | ||
</div> | ||
<label className="add"> | ||
Add{' '} | ||
<input | ||
type="number" | ||
min="-100" | ||
max="100" | ||
value={mod.reward} | ||
onChange={this.onChangeReward.bind(this, index)} | ||
/> | ||
% | ||
</label> | ||
return ( | ||
<div key={index} className="mod"> | ||
<Button className="delete-button" onClick={this.deleteMod.bind(this, index)}> | ||
× | ||
</Button> | ||
<label>When passing on attempt</label> | ||
<div className="slider-container"> | ||
<Slider | ||
domain={[1, upperRange]} | ||
values={[lowerVal, upperVal]} | ||
step={1} | ||
onChange={this.onChangeSlider.bind(this, index)} | ||
/> | ||
</div> | ||
) | ||
})} | ||
{this.state.mods.length < 20 ? <Button onClick={this.onAddMod}>Add Mod</Button> : null} | ||
</div> | ||
<div className="slider-inputs"> | ||
<input | ||
type="text" | ||
value={range.min} | ||
className="min-input" | ||
onChange={this.onChangeLower.bind(this, index)} | ||
/> | ||
through | ||
<input | ||
type="text" | ||
value={range.max} | ||
className="max-input" | ||
onChange={this.onChangeUpper.bind(this, index)} | ||
/> | ||
</div> | ||
<label className="add"> | ||
Add{' '} | ||
<input | ||
type="number" | ||
min="-100" | ||
max="100" | ||
value={mod.reward} | ||
onChange={this.onChangeReward.bind(this, index)} | ||
/> | ||
% | ||
</label> | ||
</div> | ||
) | ||
})} | ||
{this.state.mods.length < 20 ? <Button onClick={this.onAddMod}>Add Mod</Button> : null} | ||
</div> | ||
</SimpleDialog> | ||
</div> | ||
) | ||
@@ -168,0 +190,0 @@ } |
@@ -36,7 +36,11 @@ import Common from 'obojobo-document-engine/src/scripts/common' | ||
let submitButtonText = 'Loading ...' | ||
if (!this.props.isAttemptComplete) { | ||
submitButtonText = 'Submit (Not all questions have been answered)' | ||
} else if (!this.props.isFetching) { | ||
submitButtonText = 'Submit' | ||
let buttonLabel | ||
let buttonAriaLabel | ||
if (this.props.isAttemptSubmitting) { | ||
buttonLabel = buttonAriaLabel = 'Loading ...' | ||
} else if (!this.props.isAttemptReadyToSubmit) { | ||
buttonLabel = 'Submit' | ||
buttonAriaLabel = 'Submit (Not all questions have been saved)' | ||
} else { | ||
buttonLabel = buttonAriaLabel = 'Submit' | ||
} | ||
@@ -49,6 +53,12 @@ | ||
<Button | ||
disabled={this.props.isFetching} | ||
ariaLabel={buttonAriaLabel} | ||
disabled={this.props.isAttemptSubmitting} | ||
onClick={this.props.onClickSubmit} | ||
value={submitButtonText} | ||
value={buttonLabel} | ||
/> | ||
{!this.props.isAttemptReadyToSubmit ? ( | ||
<span aria-hidden className="incomplete-notice"> | ||
(Not all questions have been saved) | ||
</span> | ||
) : null} | ||
</div> | ||
@@ -55,0 +65,0 @@ </div> |
{ | ||
"name": "obojobo-sections-assessment", | ||
"version": "12.0.0", | ||
"version": "12.1.0-alpha.0", | ||
"license": "AGPL-3.0-only", | ||
@@ -63,3 +63,3 @@ "description": "Assessment section for Obojobo", | ||
}, | ||
"gitHead": "8bf5b97eaf88a31f8fae5e30d93499d22410c660" | ||
"gitHead": "b66a1dd5cc592400994cad047a6c13fcc1bc0e1f" | ||
} |
@@ -40,2 +40,6 @@ const AssessmentModel = require('../models/assessment') | ||
if (attempt.completedAt !== null) { | ||
throw Error('Cannot end an attempt that has already ended') | ||
} | ||
const attemptNumber = await AssessmentModel.getAttemptNumber( | ||
@@ -42,0 +46,0 @@ attempt.userId, |
@@ -14,3 +14,3 @@ const oboEvents = require('obojobo-express/server/obo_events') | ||
if (!event.payload.assessmentId) throw Error('Missing Assessment Id') | ||
if (!event.payload.questionId) throw Error('Missing Question ID') | ||
if (!event.payload.questionId) throw Error('Missing Question Id') | ||
if (!event.payload.response) throw Error('Missing Response') | ||
@@ -17,0 +17,0 @@ |
import './viewer-component.scss' | ||
import AttemptIncompleteDialog from './components/attempt-incomplete-dialog' | ||
import AssessmentDialog from './components/assessment-dialog' | ||
import Common from 'obojobo-document-engine/src/scripts/common' | ||
@@ -12,10 +12,24 @@ import { FOCUS_ON_ASSESSMENT_CONTENT } from './assessment-event-constants' | ||
import AssessmentMachineStates from 'obojobo-document-engine/src/scripts/viewer/stores/assessment-store/assessment-machine-states' | ||
const { OboComponent } = Viewer.components | ||
const { Dispatcher } = Common.flux | ||
const { ModalUtil } = Common.util | ||
const { Dialog } = Common.components.modal | ||
const { Spinner } = Common.components | ||
const { ModalPortal } = Common.components.modal | ||
const { AssessmentUtil } = Viewer.util | ||
const { NavUtil, FocusUtil } = Viewer.util | ||
const { NavUtil, FocusUtil, CurrentAssessmentStates } = Viewer.util | ||
const { | ||
IN_ATTEMPT, | ||
SEND_RESPONSES_SUCCESSFUL, | ||
SEND_RESPONSES_FAILED, | ||
END_ATTEMPT_FAILED, | ||
STARTING_ATTEMPT, | ||
RESUMING_ATTEMPT, | ||
SENDING_RESPONSES, | ||
ENDING_ATTEMPT, | ||
IMPORTING_ATTEMPT | ||
} = AssessmentMachineStates | ||
class Assessment extends React.Component { | ||
@@ -25,7 +39,4 @@ constructor(props) { | ||
this.state = { | ||
isFetchingEndAttempt: false, | ||
currentStep: Assessment.getCurrentStep(props) | ||
curStep: Assessment.getStep(props) | ||
} | ||
this.onEndAttempt = this.onEndAttempt.bind(this) | ||
this.onAttemptEnded = this.onAttemptEnded.bind(this) | ||
this.endAttempt = this.endAttempt.bind(this) | ||
@@ -42,8 +53,8 @@ this.onClickSubmit = this.onClickSubmit.bind(this) | ||
static getDerivedStateFromProps(nextProps) { | ||
const curStep = Assessment.getCurrentStep(nextProps) | ||
const curStep = Assessment.getStep(nextProps) | ||
return { curStep } | ||
} | ||
static getCurrentStep(props) { | ||
const assessment = AssessmentUtil.getAssessmentForModel( | ||
static getStep(props) { | ||
const state = AssessmentUtil.getAssessmentMachineStateForModel( | ||
props.moduleData.assessmentState, | ||
@@ -53,57 +64,31 @@ props.model | ||
if (assessment === null) { | ||
return 'pre-test' | ||
} | ||
switch (state) { | ||
case IN_ATTEMPT: | ||
case SEND_RESPONSES_SUCCESSFUL: | ||
case SEND_RESPONSES_FAILED: | ||
case END_ATTEMPT_FAILED: | ||
case SENDING_RESPONSES: | ||
case ENDING_ATTEMPT: { | ||
return 'test' | ||
} | ||
if (assessment.current !== null) { | ||
return 'test' | ||
} | ||
case STARTING_ATTEMPT: | ||
case RESUMING_ATTEMPT: | ||
case IMPORTING_ATTEMPT: { | ||
return 'loading' | ||
} | ||
if (assessment.attempts.length > 0) { | ||
return 'post-test' | ||
default: | ||
return AssessmentUtil.getNumberOfAttemptsCompletedForModel( | ||
props.moduleData.assessmentState, | ||
props.model | ||
) === 0 | ||
? 'pre-test' | ||
: 'post-test' | ||
} | ||
return 'pre-test' | ||
} | ||
componentWillUnmount() { | ||
// make sure navutil know's we're not in assessment any more | ||
NavUtil.resetContext() | ||
Dispatcher.off('assessment:endAttempt', this.onEndAttempt) | ||
Dispatcher.off('assessment:attemptEnded', this.onAttemptEnded) | ||
} | ||
componentDidMount() { | ||
Dispatcher.on('assessment:endAttempt', this.onEndAttempt) | ||
Dispatcher.on('assessment:attemptEnded', this.onAttemptEnded) | ||
// if we're in an active attempt - notify the navUtil we're in Assessment | ||
const attemptInfo = AssessmentUtil.getCurrentAttemptForModel( | ||
getCurrentAttemptStatus() { | ||
return AssessmentUtil.getCurrentAttemptStatus( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
if (attemptInfo) { | ||
NavUtil.setContext(`assessment:${attemptInfo.assessmentId}:${attemptInfo.attemptId}`) | ||
} | ||
} | ||
componentDidUpdate(_, prevState) { | ||
if (prevState.curStep !== this.state.curStep) { | ||
Dispatcher.trigger('viewer:scrollToTop') | ||
FocusUtil.focusOnNavTarget() | ||
} | ||
} | ||
onEndAttempt() { | ||
this.setState({ isFetchingEndAttempt: true }) | ||
} | ||
onAttemptEnded() { | ||
this.setState({ isFetchingEndAttempt: false }) | ||
} | ||
isAttemptComplete() { | ||
return AssessmentUtil.isCurrentAttemptComplete( | ||
this.props.moduleData.assessmentState, | ||
this.props.moduleData.questionState, | ||
@@ -115,6 +100,12 @@ this.props.model, | ||
isAssessmentComplete() { | ||
return !AssessmentUtil.hasAttemptsRemaining( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
isAttemptReadyToSubmit() { | ||
return this.getCurrentAttemptStatus() === CurrentAssessmentStates.READY_TO_SUBMIT | ||
} | ||
isAttemptSubmitting() { | ||
return ( | ||
AssessmentUtil.getAssessmentMachineStateForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) === ENDING_ATTEMPT | ||
) | ||
@@ -124,62 +115,9 @@ } | ||
onClickSubmit() { | ||
// disable multiple clicks | ||
if (this.state.isFetchingEndAttempt) return | ||
if (!this.isAttemptComplete()) { | ||
ModalUtil.show(<AttemptIncompleteDialog onSubmit={this.endAttempt} />) | ||
return | ||
} | ||
const remainAttempts = AssessmentUtil.getAttemptsRemaining( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
AssessmentUtil.forceSendResponsesForCurrentAttempt( | ||
this.props.model, | ||
this.props.moduleData.navState.context | ||
) | ||
if (remainAttempts === 1) { | ||
ModalUtil.show( | ||
<Dialog | ||
width="32rem" | ||
title="This is your last attempt" | ||
buttons={[ | ||
{ | ||
value: 'Cancel', | ||
altAction: true, | ||
default: true, | ||
onClick: ModalUtil.hide | ||
}, | ||
{ | ||
value: 'OK - Submit Last Attempt', | ||
onClick: this.endAttempt | ||
} | ||
]} | ||
> | ||
<p>{"You won't be able to submit another attempt after this one."}</p> | ||
</Dialog> | ||
) | ||
} else { | ||
ModalUtil.show( | ||
<Dialog | ||
width="32rem" | ||
title="Just to confirm..." | ||
buttons={[ | ||
{ | ||
value: 'Cancel', | ||
altAction: true, | ||
default: true, | ||
onClick: ModalUtil.hide | ||
}, | ||
{ | ||
value: 'OK - Submit', | ||
onClick: this.endAttempt | ||
} | ||
]} | ||
> | ||
<p>Are you ready to submit?</p> | ||
</Dialog> | ||
) | ||
} | ||
} | ||
endAttempt() { | ||
ModalUtil.hide() | ||
return AssessmentUtil.endAttempt({ | ||
@@ -192,36 +130,68 @@ model: this.props.model, | ||
exitAssessment() { | ||
const scoreAction = this.getScoreAction() | ||
getScoreAction() { | ||
return this.props.model.modelState.scoreActions.getActionForScore( | ||
AssessmentUtil.getAssessmentScoreForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
) | ||
} | ||
switch (scoreAction.action.value) { | ||
case '_next': | ||
return NavUtil.goNext() | ||
getAssessmentComponent() { | ||
switch (this.state.curStep) { | ||
case 'pre-test': | ||
return ( | ||
<PreTest model={this.props.model.children.at(0)} moduleData={this.props.moduleData} /> | ||
) | ||
case '_prev': | ||
return NavUtil.goPrev() | ||
case 'loading': | ||
return ( | ||
<div className="loading-assessment"> | ||
<Spinner /> | ||
</div> | ||
) | ||
default: | ||
return NavUtil.goto(scoreAction.action.value) | ||
case 'test': | ||
return ( | ||
<Test | ||
model={this.props.model.children.at(1)} | ||
moduleData={this.props.moduleData} | ||
onClickSubmit={this.onClickSubmit} | ||
isAttemptReadyToSubmit={this.isAttemptReadyToSubmit()} | ||
isAttemptSubmitting={this.isAttemptSubmitting()} | ||
/> | ||
) | ||
case 'post-test': | ||
return ( | ||
<PostTest | ||
ref={this.childRef} | ||
model={this.props.model} | ||
moduleData={this.props.moduleData} | ||
scoreAction={this.getScoreAction()} | ||
/> | ||
) | ||
} | ||
} | ||
getScoreAction() { | ||
const assessmentScore = AssessmentUtil.getAssessmentScoreForModel( | ||
componentWillUnmount() { | ||
NavUtil.resetContext() | ||
} | ||
componentDidMount() { | ||
// If we're in an active attempt - notify the navUtil we're in Assessment | ||
const attemptInfo = AssessmentUtil.getCurrentAttemptForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
const scoreAction = this.props.model.modelState.scoreActions.getActionForScore(assessmentScore) | ||
if (scoreAction) { | ||
return scoreAction | ||
if (attemptInfo) { | ||
NavUtil.setContext(`assessment:${attemptInfo.assessmentId}:${attemptInfo.attemptId}`) | ||
} | ||
} | ||
return { | ||
from: 0, | ||
to: 100, | ||
message: '', | ||
action: { | ||
type: 'unlock', | ||
value: '_next' | ||
} | ||
componentDidUpdate(_, prevState) { | ||
if (prevState.curStep !== this.state.curStep) { | ||
Dispatcher.trigger('viewer:scrollToTop') | ||
FocusUtil.focusOnNavTarget() | ||
} | ||
@@ -231,36 +201,7 @@ } | ||
render() { | ||
if (this.props.moduleData.assessmentState.attemptHistoryLoadState !== 'loaded') { | ||
return 'Loading...' | ||
} | ||
const assessment = AssessmentUtil.getAssessmentForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
) | ||
const childEl = (() => { | ||
switch (this.state.curStep) { | ||
case 'pre-test': | ||
return ( | ||
<PreTest model={this.props.model.children.at(0)} moduleData={this.props.moduleData} /> | ||
) | ||
case 'test': | ||
return ( | ||
<Test | ||
model={this.props.model.children.at(1)} | ||
moduleData={this.props.moduleData} | ||
onClickSubmit={this.onClickSubmit} | ||
isAttemptComplete={this.isAttemptComplete()} | ||
isFetching={this.state.isFetchingEndAttempt} | ||
/> | ||
) | ||
case 'post-test': | ||
return ( | ||
<PostTest | ||
ref={this.childRef} | ||
model={this.props.model} | ||
moduleData={this.props.moduleData} | ||
scoreAction={this.getScoreAction()} | ||
/> | ||
) | ||
} | ||
})() | ||
return ( | ||
@@ -272,3 +213,24 @@ <OboComponent | ||
> | ||
{childEl} | ||
{this.getAssessmentComponent()} | ||
<ModalPortal> | ||
<AssessmentDialog | ||
endAttempt={this.endAttempt} | ||
assessmentMachineState={AssessmentUtil.getAssessmentMachineStateForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
)} | ||
currentAttemptStatus={this.getCurrentAttemptStatus()} | ||
assessmentModel={this.props.model} | ||
assessment={assessment} | ||
importableScore={AssessmentUtil.getImportableScoreForModel( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
)} | ||
numAttemptsRemaining={AssessmentUtil.getAttemptsRemaining( | ||
this.props.moduleData.assessmentState, | ||
this.props.model | ||
)} | ||
/> | ||
</ModalPortal> | ||
</OboComponent> | ||
@@ -275,0 +237,0 @@ ) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
245648
85
6350
2