@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
164 lines (147 loc) • 5 kB
JavaScript
import React from 'react'
import findIndex from 'lodash/findIndex'
import {each, rule, onSelf} from 'instructure-validations'
import {Text} from '@instructure/ui-text'
import InteractionType from '../InteractionType'
import {RICH_FILL_BLANK_SLUG} from '../../interaction_slugs'
import {noDuplicatesMessage, presenceMessage} from '../../util/validationHelpers'
import t from '@instructure/quiz-i18n/format-message'
// ================
// ================
// VALIDATIONS
// ================
// ================
function scoringDatumValidator(val) {
let editDistanceRules = []
if (val.scoringAlgorithm === 'TextCloseEnough') {
editDistanceRules = [
rule('numeric', {
message: t('Levenshtein Distance must be a number'),
minMessage: t('Levenshtein Distance must be greater than zero'),
maxMessage: t('Levenshtein Distance must be less than the length of the blank text'),
requireInteger: true,
integerMessage: t('Levenshtein Distance must be an integer'),
min: 1,
max: val.scoringData.blankText.length,
}),
]
}
const textInChoicesRules =
val.scoringAlgorithm === 'TextInChoices'
? // each is for multiple specified correct answers
[
each(rule('presence', {message: presenceMessage(t('Correct value'))})),
onSelf(
rule('listSize', {
minMessage: t('You must have an answer'),
min: 1,
allowNullOrUndefined: true,
}),
),
]
: [onSelf(rule('presence', {message: presenceMessage(t('Correct value'))}))]
return {
scoringData: {
editDistance: editDistanceRules,
value: textInChoicesRules,
},
}
}
// ================
// ================
// RECORD
// ================
// ================
export default class RichFillBlankInteractionType extends InteractionType {
constructor(obj) {
super()
super.initializeProps(obj)
}
slug = RICH_FILL_BLANK_SLUG
hideResultStem = true
translatedName = t('Fill in the Blank')
getDefaultScoringData = () => ({
workingItemBody: '',
value: [],
})
getDefaultInteractionData = () => ({
blanks: [],
})
static baseValidations() {
// This overrides the need for itemBody to exist (it gets derived on quiz_api from workingItemBody).
// Instead we validate that workingItemBody exists within scoringData.
return {}
}
static validations = (data = {}) => {
const eachBlankValidator = {
choices: [
onSelf(
rule('listSize', {
minMessage: t('You must have more than one choice'),
min: 2,
allowNullOrUndefined: true,
}),
),
rule('noDuplicates', {field: 'itemBody', message: noDuplicatesMessage(t('Choice'))}),
each({
itemBody: [rule('presence', {message: presenceMessage(t('Choice'))})],
}),
],
}
const eachWordBankChoiceValidator = {
itemBody: [rule('presence', {message: presenceMessage(t('Distractor'))})],
}
const allBlanksValidator = onSelf(
rule('listSize', {
minMessage: t('You must have a blank. e.g.: "Roses are `red`, violets are `blue`"'),
min: 1,
}),
)
const allWordBanksValidator = onSelf(
rule('listSize', {minMessage: t('The word bank must have more than one item'), min: 2}),
)
return {
interactionData: {
blanks: [allBlanksValidator, each(eachBlankValidator)],
...(data.interactionData.wordBankChoices && {
wordBankChoices: [allWordBanksValidator, each(eachWordBankChoiceValidator)],
}),
},
scoringData: {
workingItemBody: [rule('presence', {message: presenceMessage(t('Question Stem'))})],
value: [each(scoringDatumValidator)],
},
}
}
getDefaultUserResponse() {
return {value: []}
}
hasResponse(resp, intData) {
const numBlanks = intData.blanks.length
const numWithResponse = resp.filter(blank => blank.value).length
return numBlanks === numWithResponse
}
getRenderedResponse = (responseValue, interactionData = {blanks: []}) => {
return (
<Text color="primary">
{responseValue
.map(value => {
const blankPosition = findIndex(interactionData.blanks, blank => blank.id === value.id)
const blank = blankPosition >= 0 && interactionData.blanks[blankPosition]
let valueText = ''
if (blank && blank.choices) {
const blankChoice = blank.choices.find(choice => choice.id === value.value)
valueText = blankChoice ? blankChoice.itemBody : ''
} else if (blank) {
valueText = value.value
}
return t('Blank {blankPosition, number}: {valueText}', {
blankPosition: blankPosition >= 0 ? blankPosition + 1 : '',
valueText,
})
})
.join(', ')}
</Text>
)
}
}