UNPKG

@oat-sa/tao-item-runner-qti

Version:
950 lines (874 loc) 39.4 kB
/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2017 (original work) Open Assessment Technologies SA; * */ import _ from 'lodash'; import gamp from 'lib/gamp/gamp'; import responseHelper from 'taoQtiItem/qtiItem/helper/response'; import OutcomeDeclaration from 'taoQtiItem/qtiItem/core/variables/OutcomeDeclaration'; /** * This variable allow to globally define if the minCHoice needs to be taken into consideration. * Standard-wise, it must definitely be considered. * However, the item delivery lifecycle currently does not consider the minChoice constraint during delivery. * It is thus currently set to true. After the correct behaviour is implemented, we should remove this variables. * @type {boolean} * @private */ var _ignoreMinChoice = true; var pairExists = function pairExists(collection, pair) { if (pair.length !== 2) { return false; } return collection[pair[0] + ' ' + pair[1]] || collection[pair[1] + ' ' + pair[0]]; }; const externalScoredValues = ['human', 'externalMachine']; export default { /** * Set the normal maximum to the item * @param {Object} item - the standard qti item model object */ setNormalMaximum(item) { var normalMaximum, scoreOutcome = item.getOutcomeDeclaration('SCORE'); //try setting the computed normal maximum only if the processing type is known, i.e. 'templateDriven' if ( scoreOutcome && item.responseProcessing && item.responseProcessing.processingType === 'templateDriven' && !externalScoredValues.includes(scoreOutcome.attr('externalScored')) ) { const interactions = item.getInteractions(); normalMaximum = _.reduce( interactions, function (acc, interaction) { var interactionMaxScore = interaction.getNormalMaximum(); if (_.isNumber(interactionMaxScore)) { return gamp.add(acc, interactionMaxScore); } else { return false; } }, 0 ); if (_.isNumber(normalMaximum)) { scoreOutcome.attr('normalMaximum', normalMaximum); } else { scoreOutcome.removeAttr('normalMaximum'); } } }, /** * Set the maximum score of the item * @param {Object} item - the standard qti item model object */ setMaxScore(item) { var hasInvalidInteraction = false, scoreOutcome = item.getOutcomeDeclaration('SCORE'), maxScore, maxScoreOutcome; if (!scoreOutcome) { // add new score outcome if not already defined scoreOutcome = new OutcomeDeclaration({ cardinality: 'single', baseType: 'float', normalMinimum: 0.0, normalMaximum: 0.0 }); item.addOutcomeDeclaration(scoreOutcome); scoreOutcome.buildIdentifier('SCORE', false); } const customOutcomes = _(item.getOutcomes()).filter(function (outcome) { return outcome.id() !== 'SCORE' && outcome.id() !== 'MAXSCORE'; }); //try setting the computed normal maximum only if the processing type is known, i.e. 'templateDriven' if (scoreOutcome && item.responseProcessing && item.responseProcessing.processingType === 'templateDriven') { const interactions = item.getInteractions(); if (externalScoredValues.includes(scoreOutcome.attr('externalScored'))) { maxScore = scoreOutcome.attr('normalMaximum') || 0; } else { maxScore = _.reduce( interactions, function (acc, interaction) { var interactionMaxScore = interaction.getNormalMaximum(); if (_.isNumber(interactionMaxScore)) { return gamp.add(acc, interactionMaxScore); } else { hasInvalidInteraction = true; return acc; } }, 0 ); if (customOutcomes.size()) { maxScore = customOutcomes.reduce(function (acc, outcome) { return gamp.add(acc, parseFloat(outcome.attr('normalMaximum') || 0)); }, maxScore); } } maxScoreOutcome = item.getOutcomeDeclaration('MAXSCORE'); if (!hasInvalidInteraction || customOutcomes.size()) { if (!maxScoreOutcome) { //add new outcome maxScoreOutcome = new OutcomeDeclaration({ cardinality: 'single', baseType: 'float' }); //attach the outcome to the item before generating item-level unique id item.addOutcomeDeclaration(maxScoreOutcome); maxScoreOutcome.buildIdentifier('MAXSCORE', false); } maxScoreOutcome.setDefaultValue(maxScore); } //handle special case when MAXSCORE is set up manually for some interaction like ExtendedText if (hasInvalidInteraction && maxScoreOutcome) { if (maxScoreOutcome.attributes && maxScoreOutcome.attributes.externalScored) { if (_.isUndefined(maxScoreOutcome.defaultValue)) { maxScoreOutcome.setDefaultValue(1); } } else { item.removeOutcome('MAXSCORE'); } } const isAllResponseProcessingRulesNone = !interactions.some(interaction => { const responseDeclaration = interaction.getResponseDeclaration(); const template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); return template !== 'NONE'; }); const outcomesWithExternalScored = customOutcomes.filter(outcome => { return externalScoredValues.includes(outcome.attr('externalScored')); }); const isResponsesEmpty = _.isEmpty(item.responses); // remove MAXSCORE and SCORE outcome variables when all interactions are configured with none response processing rule, // and the externalScored property of the SCORE variable is set to None // and there are no other outcome variables with externalScored property set to human or externalMachine // or in case all interactions are without responses if ( (!scoreOutcome.attr('externalScored') && isAllResponseProcessingRulesNone && outcomesWithExternalScored.size() === 0) || isResponsesEmpty ) { item.removeOutcome('MAXSCORE'); item.removeOutcome('SCORE'); } // remove custom outcomes if all interactions are without responses if (isResponsesEmpty) { customOutcomes.forEach(outcome => { item.removeOutcome(outcome.id()); }); } } }, /** * Sort an array of associable choices by its matchMax attr value * @param {Array} choiceCollection * @returns {Array} */ getMatchMaxOrderedChoices(choiceCollection) { return _(choiceCollection) .map(function (choice) { var matchMax = parseInt(choice.attr('matchMax'), 10); if (_.isNaN(matchMax)) { matchMax = 0; } return { matchMax: matchMax === 0 ? Infinity : matchMax, id: choice.id() }; }) .sortBy('matchMax') .reverse() .valueOf(); }, /** * Compute the maximum score of a "choice" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ choiceInteractionBased(interaction, options) { var responseDeclaration = interaction.getResponseDeclaration(); var mapDefault = parseFloat(responseDeclaration.mappingAttributes.defaultValue || 0); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var max, maxChoice, minChoice, scoreMaps, requiredChoiceCount, totalAnswerableResponse, sortedMapEntries, missingMapsCount; options = _.defaults(options || {}, { maxChoices: 0, minChoices: 0 }); maxChoice = parseInt(interaction.attr('maxChoices') || options.maxChoices, 10); minChoice = _ignoreMinChoice ? 0 : parseInt(interaction.attr('minChoices') || options.minChoices, 10); if (maxChoice && minChoice && maxChoice < minChoice) { return 0; } if (template === 'MATCH_CORRECT') { if ( maxChoice && _.isArray(responseDeclaration.correctResponse) && (responseDeclaration.correctResponse.length > maxChoice || responseDeclaration.correctResponse.length < minChoice) ) { //max choice does not enable selecting the correct responses max = 0; } else if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && !responseDeclaration.correctResponse.length) ) { //no correct response defined -> score always zero max = 0; } else { max = 1; } } else if (template === 'MAP_RESPONSE') { //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } //prepare constraint params requiredChoiceCount = minChoice; totalAnswerableResponse = maxChoice === 0 ? Infinity : maxChoice; //sort the score map entries by the score scoreMaps = _.values(responseDeclaration.mapEntries); sortedMapEntries = _(scoreMaps) .map(function (v) { return parseFloat(v); }) .sortBy() .reverse() .valueOf() .slice(0, totalAnswerableResponse); //if there is not enough map defined, compared to the minChoice constraint, fill in the rest of required choices with the default map missingMapsCount = minChoice - sortedMapEntries.length; _.times(missingMapsCount, function () { sortedMapEntries.push(mapDefault); }); //if the map default is positive, the optimal strategy involves using as much mapDefault as possible if (mapDefault && mapDefault > 0) { if (maxChoice) { missingMapsCount = maxChoice - sortedMapEntries.length; } else { missingMapsCount = _.size(interaction.getChoices()) - sortedMapEntries.length; } if (missingMapsCount > 0) { _.times(missingMapsCount, function () { sortedMapEntries.push(mapDefault); }); } } //calculate the maximum reachable score by choice map max = sortedMapEntries.reduce(function (acc, v) { var score = v; if (score < 0 && requiredChoiceCount <= 0) { //if the score is negative check if we have the choice not to pick it score = 0; } requiredChoiceCount--; return gamp.add(acc, score); }, 0); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'MAP_RESPONSE_POINT') { //map point response processing does not work on choice based interaction max = 0; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "order" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ orderInteractionBased(interaction) { var minChoice = _ignoreMinChoice ? 0 : parseInt(interaction.attr('minChoices') || 0, 10); var maxChoice = parseInt(interaction.attr('maxChoices') || 0, 10); var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var max; if (maxChoice && minChoice && maxChoice < minChoice) { return 0; } if (template === 'MATCH_CORRECT') { if ( (_.isArray(responseDeclaration.correctResponse) && maxChoice && responseDeclaration.correctResponse.length > maxChoice) || (minChoice && responseDeclaration.correctResponse.length < minChoice) ) { //max choice does not enable selecting the correct responses max = 0; } else if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && !responseDeclaration.correctResponse.length) ) { //no correct response defined -> score always zero max = 0; } else { max = 1; } } else if (template === 'MAP_RESPONSE' || template === 'MAP_RESPONSE_POINT') { //map response processing does not work on order based interaction max = 0; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "associate" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ associateInteractionBased(interaction, options) { var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var maxAssoc = parseInt(interaction.attr('maxAssociations') || 0, 10); var minAssoc = _ignoreMinChoice ? 0 : parseInt(interaction.attr('minAssociations') || 0, 10); var mapDefault = parseFloat(responseDeclaration.mappingAttributes.defaultValue || 0); var max, requiredAssoc, totalAnswerableResponse, usedChoices, choicesIdentifiers, sortedMapEntries, i, allPossibleMapEntries, infiniteScoringPair; options = _.defaults(options || {}, { possiblePairs: [], checkInfinitePair: false }); if (maxAssoc && minAssoc && maxAssoc < minAssoc) { return 0; } if (template === 'MATCH_CORRECT') { if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && (!responseDeclaration.correctResponse.length || (maxAssoc && responseDeclaration.correctResponse.length > maxAssoc) || (minAssoc && responseDeclaration.correctResponse.length < minAssoc))) ) { //no correct response defined -> score always zero max = 0; } else { max = 1; //is possible until proven otherwise //get the list of choices used in map entries choicesIdentifiers = []; _.forEach(responseDeclaration.correctResponse, function (pair) { var choices; if (!_.isString(pair)) { return; } choices = pair.trim().split(' '); if (_.isArray(choices) && choices.length === 2) { choicesIdentifiers.push(choices[0].trim()); choicesIdentifiers.push(choices[1].trim()); } }); //check if the choices usage are possible within the constraint defined in the interaction _.forEach(_.countBy(choicesIdentifiers), function (count, identifier) { var matchMax; var choice = interaction.getChoiceByIdentifier(identifier); if (!choice) { max = 0; return false; } matchMax = parseInt(choice.attr('matchMax'), 10); if (matchMax && matchMax < count) { max = 0; return false; } }); } } else if (template === 'MAP_RESPONSE') { requiredAssoc = minAssoc; totalAnswerableResponse = maxAssoc === 0 ? Infinity : maxAssoc; usedChoices = {}; //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } allPossibleMapEntries = _.clone(responseDeclaration.mapEntries); if (mapDefault && mapDefault > 0) { _.forEachRight(options.possiblePairs, function (pair) { if (!pairExists(allPossibleMapEntries, pair)) { allPossibleMapEntries[pair[0] + ' ' + pair[1]] = mapDefault; } }); } //get the sorted list of mapentries ordered by the score sortedMapEntries = _(allPossibleMapEntries) .map(function (score, pair) { return { score: parseFloat(score), pair: pair }; }) .sortBy('score') .reverse() .filter(function (mapEntry) { var pair = mapEntry.pair; var choices, choiceId, choice, _usedChoices; if (!_.isString(pair)) { return false; } //check that the pair is possible in term of matchMax choices = pair.trim().split(' '); if (_.isArray(choices) && choices.length === 2) { //clone the global used choices array to brings the changes in that object first before storing in the actual object _usedChoices = _.cloneDeep(usedChoices); for (i = 0; i < 2; i++) { choiceId = choices[i]; //collect choices usage to check if the pair is possible if (!_usedChoices[choiceId]) { choice = interaction.getChoiceByIdentifier(choiceId); if (!choice) { //unexisting choice, skip return false; } _usedChoices[choiceId] = { used: 0, max: parseInt(choice.attr('matchMax'), 10) }; } if ( _usedChoices[choiceId].max && _usedChoices[choiceId].used === _usedChoices[choiceId].max ) { //skip return false; } else { _usedChoices[choiceId].used++; } } //identify the edge case when we can get infinite association pair that create an infinite score infiniteScoringPair = infiniteScoringPair || (options.checkInfinitePair && mapEntry.score > 0 && _usedChoices[choices[0]].max === 0 && _usedChoices[choices[1]].max === 0); //update the global used choices array _.assign(usedChoices, _usedChoices); return true; } else { //is not a correct response pair return false; } }) .valueOf() .slice(0, totalAnswerableResponse); //infinite score => no normalMaximum should be generated for it if (infiniteScoringPair) { return false; } //reduce the ordered list of map entries to calculate the max score max = sortedMapEntries.reduce(function (acc, v) { var score = v.score; if (v.score < 0 && requiredAssoc <= 0) { //if the score is negative check if we have the choice not to pick it score = 0; } requiredAssoc--; return gamp.add(acc, score); }, 0); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'MAP_RESPONSE_POINT') { max = 0; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "gap match" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ gapMatchInteractionBased(interaction) { var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var maxAssoc = 0; var minAssoc = 0; var mapDefault = parseFloat(responseDeclaration.mappingAttributes.defaultValue || 0); var max, skippableWrongResponse, totalAnswerableResponse, usedChoices, usedGaps, group1, group2, allPossibleMapEntries; var getMatchMaxOrderedChoices = function getMatchMaxOrderedChoices(choiceCollection) { return _(choiceCollection) .map(function (choice) { return { matchMax: choice.attr('matchMax') === 0 ? Infinity : choice.attr('matchMax') || 0, id: choice.id() }; }) .sortBy('matchMax') .reverse() .valueOf(); }; var calculatePossiblePairs = function calculatePossiblePairs(gapMatchInteraction) { //get max number of pairs var pairs = []; var matchSet1 = getMatchMaxOrderedChoices(gapMatchInteraction.getChoices()); var matchSet2 = getMatchMaxOrderedChoices(gapMatchInteraction.getGaps()); _.forEach(matchSet1, function (choice1) { _.forEach(matchSet2, function (choice2) { pairs.push([choice1.id, choice2.id]); }); }); return pairs; }; if (template === 'MATCH_CORRECT') { if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && !responseDeclaration.correctResponse.length) ) { //no correct response defined -> score always zero max = 0; } else { max = 1; //is possible until proven otherwise group1 = []; group2 = []; _.forEach(responseDeclaration.correctResponse, function (pair) { var choices; if (!_.isString(pair)) { return; } choices = pair.trim().split(' '); if (_.isArray(choices) && choices.length === 2) { group1.push(choices[0].trim()); group2.push(choices[1].trim()); } }); _.forEach(_.countBy(group1), function (count, identifier) { var choice = interaction.getChoiceByIdentifier(identifier); var matchMax = parseInt(choice.attr('matchMax'), 10); if (matchMax && matchMax < count) { max = 0; return false; } }); _.forEach(_.countBy(group2), function (count) { var matchMax = 1; //match max for a gap is always 1 if (matchMax && matchMax < count) { max = 0; return false; } }); } } else if (template === 'MAP_RESPONSE') { skippableWrongResponse = minAssoc === 0 ? Infinity : minAssoc; totalAnswerableResponse = maxAssoc === 0 ? Infinity : maxAssoc; usedChoices = {}; usedGaps = {}; //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } allPossibleMapEntries = _.clone(responseDeclaration.mapEntries); if (mapDefault && mapDefault > 0) { _.forEachRight(calculatePossiblePairs(interaction), function (pair) { if (!pairExists(allPossibleMapEntries, pair)) { allPossibleMapEntries[pair[0] + ' ' + pair[1]] = mapDefault; } }); } max = _(allPossibleMapEntries) .map(function (score, pair) { return { score: parseFloat(score), pair: pair }; }) .sortBy('score') .reverse() .filter(function (mapEntry) { var pair = mapEntry.pair; var _usedChoices = _.cloneDeep(usedChoices); var choices, choiceId, gapId, choice; if (!_.isString(pair)) { return false; } choices = pair.trim().split(' '); if (_.isArray(choices) && choices.length === 2) { choiceId = choices[0]; gapId = choices[1]; if (!_usedChoices[choiceId]) { choice = interaction.getChoiceByIdentifier(choiceId); if (!choice) { //inexisting choice, skip return false; } _usedChoices[choiceId] = { used: 0, max: parseInt(choice.attr('matchMax'), 10) }; } if (_usedChoices[choiceId].max && _usedChoices[choiceId].used === _usedChoices[choiceId].max) { //skip return false; } _usedChoices[choiceId].used++; if (!usedGaps[gapId]) { usedGaps[gapId] = { used: 0, max: 1 }; } if (usedGaps[gapId].max && usedGaps[gapId].used === usedGaps[gapId].max) { //skip return false; } usedGaps[gapId].used++; //if an only if it is ok, we merge the temporary used choices array into the global one _.assign(usedChoices, _usedChoices); return true; } else { //is not a correct response pair return false; } }) .valueOf() .slice(0, totalAnswerableResponse) .reduce(function (acc, v) { var score = v.score; if (score >= 0) { return acc + score; } else if (skippableWrongResponse > 0) { skippableWrongResponse--; return acc; } else { return acc + score; } }, 0); //console.log(usedChoices, allPossibleMapEntries, sortedMaps); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'MAP_RESPONSE_POINT') { max = false; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "select point" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ selectPointInteractionBased(interaction) { var maxChoice = parseInt(interaction.attr('maxChoices'), 10); var minChoice = _ignoreMinChoice ? 0 : parseInt(interaction.attr('minChoices'), 10); var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var max, skippableWrongResponse, totalAnswerableResponse; if (template === 'MATCH_CORRECT' || template === 'MAP_RESPONSE') { //such templates are not allowed return 0; } else if (template === 'MAP_RESPONSE_POINT') { //calculate the maximum reachable score by choice map skippableWrongResponse = minChoice === 0 ? Infinity : minChoice; totalAnswerableResponse = maxChoice === 0 ? Infinity : maxChoice; max = _(responseDeclaration.mapEntries) .map(function (v) { return parseFloat(v.mappedValue); }) .sortBy() .reverse() .valueOf() .slice(0, totalAnswerableResponse) .reduce(function (acc, v) { if (v >= 0) { return acc + v; } else if (skippableWrongResponse > 0) { skippableWrongResponse--; return acc; } else { return acc + v; } }, 0); max = parseFloat(max); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "slider" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ sliderInteractionBased(interaction) { var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var max, scoreMaps; if (template === 'MATCH_CORRECT') { if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && !responseDeclaration.correctResponse.length) ) { //no correct response defined -> score always zero max = 0; } else { max = 1; } } else if (template === 'MAP_RESPONSE') { //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } //calculate the maximum reachable score by choice map scoreMaps = _.values(responseDeclaration.mapEntries); max = _(scoreMaps) .map(function (v) { return parseFloat(v); }) .max(); max = parseFloat(max); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'MAP_RESPONSE_POINT') { max = 0; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "text entry" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ textEntryInteractionBased(interaction) { var responseDeclaration = interaction.getResponseDeclaration(); var template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); var max, scoreMaps; /** * Check that a response is possible or not according to the defined patternmask * @param {String} value * @returns {Boolean} */ var isPossibleResponse = function isPossibleResponse(value) { var patternMask = interaction.attr('patternMask'); if (patternMask) { return !!value.match(new RegExp(patternMask)); } else { //no restriction by pattern so always possible return true; } }; if (template === 'MATCH_CORRECT') { if ( !responseDeclaration.correctResponse || (_.isArray(responseDeclaration.correctResponse) && !responseDeclaration.correctResponse.length) ) { //no correct response defined -> score always zero max = 0; } else { max = isPossibleResponse(responseDeclaration.correctResponse[0]) ? 1 : 0; } } else if (template === 'MAP_RESPONSE') { //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } //calculate the maximum reachable score by choice map scoreMaps = _.values( _.filter(responseDeclaration.mapEntries, function (score, key) { return isPossibleResponse(key); }) ); max = _(scoreMaps) .map(function (v) { return parseFloat(v); }) .max(); max = parseFloat(max); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'MAP_RESPONSE_POINT') { max = 0; } else if (template === 'NONE') { // set max to zero max = 0; } return max; }, /** * Compute the maximum score of a "custom" typed interaction * @param {Object} interaction - a standard interaction model object * @returns {Number} */ customInteractionBased(interaction) { const responseDeclaration = interaction.getResponseDeclaration(); const template = responseHelper.getTemplateNameFromUri(responseDeclaration.template); let max; if (template === 'MATCH_CORRECT') { if (Array.isArray(responseDeclaration.correctResponse) && responseDeclaration.correctResponse.length) { max = 1; } else { max = 0; } } else if (template === 'MAP_RESPONSE') { //at least a map entry is required to be valid QTI if (!responseDeclaration.mapEntries || !_.size(responseDeclaration.mapEntries)) { return 0; } const values = _.values(responseDeclaration.mapEntries).map(function (v) { return parseFloat(v); }); max = _.max(values); //compare the calculated maximum with the mapping upperbound if (responseDeclaration.mappingAttributes.upperBound) { max = Math.min(max, parseFloat(responseDeclaration.mappingAttributes.upperBound || 0)); } } else if (template === 'NONE') { // set max to zero max = 0; } else { max = 0; } return max; } };