Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

obojobo-sections-assessment

Package Overview
Dependencies
Maintainers
3
Versions
114
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

obojobo-sections-assessment - npm Package Compare versions

Comparing version 12.0.0 to 12.1.0-alpha.0

components/assessment-dialog.js

6

components/attempt-incomplete-dialog.js

@@ -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&apos;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&apos;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&apos;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&apos;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&apos;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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc