UNPKG

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

Version:
335 lines (306 loc) 11.6 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) 2015-2019 (original work) Open Assessment Technlogies SA (under the project TAO-PRODUCT); * */ /** * Provides the QTI implementation for the scoring. * The provider needs to be registered into the {@link taoItems/scoring/api/scorer}. * * @author Bertrand Chevrier <bertrand@taotesting.com> */ import _ from 'lodash'; import ruleEngineFactory from 'taoQtiItem/scoring/processor/responseRules/engine'; import preProcessor from 'taoQtiItem/scoring/processor/expressions/preprocessor'; import errorHandler from 'taoQtiItem/scoring/processor/errorHandler'; /** * The mapping between PCI and QTI cardinalities */ var qtiPciCardinalities = { single: 'base', multiple: 'list', ordered: 'list', record: 'record' }; /** * Format the scoring state using the PCI response format. * * @param {Object} state - the scoring state * @returns {Object} the state formated in PCI */ var stateToPci = function stateToPci(state) { var pciState = {}; _.forEach(state, function (variable, identifier) { var pciCardinality = qtiPciCardinalities[variable.cardinality]; var baseType = variable.baseType; if (pciCardinality) { pciState[identifier] = {}; if (pciCardinality === 'base') { if (variable.value === null || typeof variable.value === 'undefined') { pciState[identifier].base = null; } else { pciState[identifier].base = {}; pciState[identifier].base[baseType] = variable.value; } } else { pciState[identifier][pciCardinality] = {}; pciState[identifier][pciCardinality][baseType] = variable.value; } } }); return pciState; }; /** * Format a pci record array : * [ * {name : 'fieldA', base : {string : 'yes'}}, * {name : 'fieldB', list : {boolean : [true, true, false]}} * ] * * into the scorer internal variable format : * { * fieldA : { * cardinality : 'single', * baseType : 'string', * value : 'yes' * }, * fieldB : { * cardinality : 'multiple', * baseType : 'boolean', * value : [true, true, false] * } * } * @param {Array} record * @returns {Object} */ var pciRecordToVariable = function pciRecordToVariable(record) { var variableObject = {}; _.forEach(record, function (value) { var ret = {}; var valueObject; if (value) { valueObject = value.base || value.list || null; if (valueObject) { _.forOwn(valueObject, function (actualValue, baseType) { var cardinality = value.base ? 'single' : 'multiple'; ret = { cardinality: cardinality, baseType: baseType, value: actualValue }; return false; //there can only be one baseType:value pair }); } variableObject[value.name] = ret; } }); return variableObject; }; /** * Reformat the mapping/areaMapping from a flat list to a structured object to anticipate changes in the serializer. * It should be deprecated once the new format is implemented. * * @param {Object} response - the QTI response declaration * @returns {Object} the formated mapping */ var reFormatMapping = function reFormatMapping(response) { var mapping; if (response.mapping && _.size(response.mapping) > 0) { mapping = { qtiClass: 'mapping', attributes: response.mappingAttributes }; mapping.mapEntries = _.map(response.mapping, function (value, key) { return { qtiClass: 'mapEntry', mapKey: key, mapValue: value, attributes: { caseSensitive: false } }; }); } if (response.areaMapping && _.size(response.areaMapping) > 0) { mapping = { qtiClass: 'areaMapping', attributes: response.mappingAttributes }; mapping.mapEntries = _.map(response.areaMapping, function (entry) { return _.extend( { qtiClass: 'areaMapEntry' }, entry ); }); } return mapping; }; /** * Creates the scoring state frome responses and item delcaration. * * @param {Object} responses - the test taker responses as RESPONSE_IDENTIFIER : PCI_RESPONSE * @param {Object} itemData - the item declaration * @returns {Object} the state * @throws {Error} when variable aren't declared correctly */ var stateBuilder = function stateBuilder(responses, itemData) { var state = {}; //load responses variables _.forEach(itemData.responses, function (response) { var responseValue; var identifier = response.attributes.identifier; var cardinality = response.attributes.cardinality; var baseType = response.attributes.baseType; var pciCardinality = qtiPciCardinalities[cardinality]; if (state[identifier]) { //throw an error return errorHandler.throw( 'scoring', new Error('Variable collision : the state already contains the response variable ' + identifier) ); } //load the declaration state[identifier] = { cardinality: cardinality, baseType: baseType, correctResponse: response.correctResponses, defaultValue: response.attributes.defaultValue || response.defaultValue }; //support both old an new mapping format if ( response.mapping && (response.mapping.qtiClass === 'mapping' || response.mapping.qtiClass === 'areaMapping') ) { state[identifier].mapping = response.mapping; } else { state[identifier].mapping = reFormatMapping(response); } //and add the current response if (responses && responses[identifier] && typeof responses[identifier][pciCardinality] !== 'undefined') { responseValue = responses[identifier][pciCardinality]; if (_.isArray(responseValue)) { //is record cardinality state[identifier].value = pciRecordToVariable(responseValue); } else if (_.isObject(responseValue)) { state[identifier].value = typeof responseValue[baseType] !== 'undefined' ? responseValue[baseType] : null; } else { state[identifier].value = null; } } }); //load outcomes variables _.forEach(itemData.outcomes, function (outcome) { var identifier = outcome.attributes.identifier; var outcomeVariable; if (state[identifier]) { //throw an error return errorHandler.throw( 'scoring', new Error('Variable collision : the state already contains the outcome variable ' + identifier) ); } outcomeVariable = { cardinality: outcome.attributes.cardinality, baseType: outcome.attributes.baseType }; if (typeof outcome.defaultValue !== 'undefined') { outcomeVariable.defaultValue = outcome.defaultValue; if (outcomeVariable.defaultValue === null && outcomeVariable.cardinality === 'single') { if (outcomeVariable.baseType === 'integer' || outcomeVariable.baseType === 'float') { outcomeVariable.value = 0; } } else { outcomeVariable.value = outcomeVariable.defaultValue; } } state[identifier] = preProcessor().parseVariable(outcomeVariable); }); return state; }; /** * Looking for custom operation used in item and load appropriate definitions * @param {Array} rules to be parsed * @param {Function} done callback on finish */ var loadCustomOperators = function loadCustomOperators(rules, done) { var supportedRules = _.filter(rules, ruleEngineFactory.isRuleSupported); var classes = []; var getCustomOperatorsClasses = function getCustomOperatorsClasses(e) { if (_.isObject(e)) { if (e.qtiClass === 'customOperator') { if (e.attributes.class) { classes.push(e.attributes.class); } else { return errorHandler.throw('scoring', new Error('Class must be specified for custom operator')); } } else { return _.forEach(e, getCustomOperatorsClasses); } } }; _.forEach(supportedRules, getCustomOperatorsClasses); if (classes.length) { window.require(classes, done); } else { done(); } }; /** * The QTI scoring provider. * * * @exports taoQtiItem/scoring/provider/qti */ var qtiScoringProvider = { /** * Process the score from the response. * * @param {Object} responses - we expect a response formated using the PCI * @param {Object} itemData - we expect the whole itemData in the QTI context. * @param {Function} done - callback with the produced outcome * @see {@link http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343} for the response format. * @this {taoItems/scoring/api/scorer} the scorer calls are delegated here, the context is the scorer's context with event mehods available. */ process: function process(responses, itemData, done) { var self = this; var state; var ruleEngine; //raise errors from inside the scoring stuffs errorHandler.listen('scoring', function onError(err) { self.trigger('error', err); }); //the state is built and formatted using the same format as processing variables, //easier to manipulate in using lodash state = stateBuilder(responses, itemData); //let's start if (itemData.responseProcessing) { loadCustomOperators(itemData.responseProcessing.responseRules, function executeEngine() { //create a ruleEngine for the given state ruleEngine = ruleEngineFactory(state); //run the engine... ruleEngine.execute(itemData.responseProcessing.responseRules); done(stateToPci(state), state); }); } else { errorHandler.throw('scoring', new Error('The item ' + itemData.identifier + ' has not responseProcessing')); done(stateToPci(state), state); } } }; export default qtiScoringProvider;