@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
357 lines (347 loc) • 13.9 kB
JavaScript
define(['lodash', 'taoQtiTest/runner/branchRule/branchRule', 'taoQtiTest/runner/helpers/map'], function (_, branchRule, mapHelper) { 'use strict';
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
branchRule = branchRule && Object.prototype.hasOwnProperty.call(branchRule, 'default') ? branchRule['default'] : branchRule;
mapHelper = mapHelper && Object.prototype.hasOwnProperty.call(mapHelper, 'default') ? mapHelper['default'] : mapHelper;
/**
* 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) 2019 (original work) Open Assessment Technologies SA ;
*/
/**
* Jump table entry definition.
*
* @typedef Jump
* @property {string} item - the identifier of the item
* @property {string} section - the identifier of the section
* @property {string} part - the identifier of the part
* @property {integer} position - the position of the jump entry, starting from 0
*/
/**
* Helper class for the offline version of the jump table which helps the navigation of the test taker.
*/
var offlineJumpTableFactory = function offlineJumpTableFactory(itemStore, responseStore) {
var testMap = {};
var jumpTable = [];
/**
* Put all the responses from the navigation parameters into the responseStore
*
* @param {Object} params
*/
function addResponsesToResponseStore(params) {
if (params.itemResponse) {
_.forEach(params.itemResponse, function (response, itemResponseIdentifier) {
var responseIdentifier = `${params.itemDefinition}.${itemResponseIdentifier}`;
_.forEach(response, function (responseEntry) {
var responseId = responseEntry && responseEntry.identifier;
if (Array.isArray(responseId)) {
responseId.forEach(function (id) {
responseStore.addResponse(responseIdentifier, id);
});
} else {
responseStore.addResponse(responseIdentifier, responseId);
}
});
});
}
}
/**
* Returns all the item identifiers in order.
*
* @param {Object} map
* @returns {Array}
*/
function getItems(map) {
return _.uniq(_.map(getSimplifiedTestMap(map), function (row) {
return row.item;
}));
}
/**
* Returns all the section identifiers in order.
*
* @param {Object} map
* @returns {Array}
*/
function getSections(map) {
return _.uniq(_.map(getSimplifiedTestMap(map), function (row) {
return row.section;
}));
}
/**
* Returns a simplified test map array, which will contain the item, section and part identifiers.
*
* @param {Object} map
* @returns {Array}
*/
function getSimplifiedTestMap(map) {
var simplifiedTestMap = [];
mapHelper.each(map, function (item, section, part) {
simplifiedTestMap.push({
item: item.id,
itemHasBranchRule: !_.isEmpty(item.branchRule),
itemBranchRule: item.branchRule,
section: section.id,
sectionHasBranchRule: !_.isEmpty(section.branchRule),
sectionBranchRule: section.branchRule,
part: part.id,
partHasBranchRule: !_.isEmpty(part.branchRule),
partBranchRule: part.branchRule
});
});
return simplifiedTestMap;
}
return {
/**
* Setter for test map
*
* @param {Object} map
* @returns {offlineJumpTableFactory}
*/
setTestMap: function setTestMap(map) {
testMap = map;
return this;
},
/**
* Build jumpTable
*
* @param {Object} testContext
* @returns {Promise}
*/
buildJumpTable: function buildJumpTable(testContext) {
const self = this;
const simplifiedTestMap = getSimplifiedTestMap(testMap);
const contextItemId = testContext ? testContext.itemIdentifier : null;
const contextItemPosition = contextItemId ? testContext.itemPosition : null;
const firstJumpItem = simplifiedTestMap[0];
if (firstJumpItem) {
this.addJump(firstJumpItem.part, firstJumpItem.section, firstJumpItem.item);
}
if (!contextItemPosition) {
return Promise.resolve();
}
function calculateNextJump() {
var lastJumpItem = self.getLastJump().item || null;
if (contextItemId !== lastJumpItem) {
return itemStore.get(lastJumpItem).then(function (item) {
const itemResponse = {};
_.forEach(item.itemState, function (state, itemStateIdentifier) {
itemResponse[itemStateIdentifier] = state.response;
});
return self.jumpToNextItem(Object.assign({}, item, {
itemResponse,
itemDefinition: item.itemIdentifier
})).then(calculateNextJump);
});
}
return Promise.resolve();
}
return calculateNextJump();
},
/**
* Put all correct responses to the responseStore
*
* @param {Object} testContext
* @returns {Promise}
*/
putCorrectResponsesInStore: function putCorrectResponsesInStore() {
const simplifiedTestMap = getSimplifiedTestMap(testMap);
const promises = [];
simplifiedTestMap.forEach(function (row) {
promises.push(itemStore.get(row.item).then(function (item) {
if (item) {
_.forEach(item.itemData.data.responses, function (response) {
var responseIdentifier = `${item.itemIdentifier}.${response.identifier}`;
responseStore.addCorrectResponse(responseIdentifier, response.correctResponses);
});
}
}).catch(function (err) {
return Promise.reject(err);
}));
});
return Promise.all(promises);
},
/**
* Initialization method for the offline jump table, which is responsible to add the first item as the first
* jump and collect the correct responses for the branching rules.
* @param {Object} [testContext] - current test context is needed in order to continue test after interruption
* @returns {Promise}
*/
init: function init(testContext) {
return this.putCorrectResponsesInStore().then(() => this.buildJumpTable(testContext));
},
/**
* Clears the jump table
*
* @returns {offlineJumpTableFactory}
*/
clearJumpTable: function clearJumpTable() {
jumpTable = [];
return this;
},
/**
* Adds a new jump into the end of the jump table
*
* @param {String} partIdentifier
* @param {String} sectionIdentifier
* @param {String} itemIdentifier
*/
addJump: function addJump(partIdentifier, sectionIdentifier, itemIdentifier) {
var self = this;
return new Promise(function (resolve) {
var lastJump = self.getLastJump();
var nextPosition = typeof lastJump.position !== 'undefined' ? lastJump.position + 1 : 0;
jumpTable.push({
item: itemIdentifier,
part: partIdentifier,
section: sectionIdentifier,
position: nextPosition
});
resolve();
});
},
/**
* Jumps into a specific position inside the jump table. The jump entry in the given position
* will be the last element of the jump table, every other entry after this entry will get deleted
*
* @param {Integer} position
* @returns {offlineJumpTableFactory}
*/
jumpTo: function jumpTo(position) {
jumpTable = jumpTable.filter(function (jump) {
return jump.position <= position;
});
return this;
},
/**
* Jumps to the next item without taking the branching rules into account
*
* @returns {Promise}
*/
jumpToSkipItem: function jumpToSkipItem() {
var self = this;
return new Promise(function (resolve) {
var simplifiedTestMap = getSimplifiedTestMap(testMap);
var lastJumpItem = self.getLastJump().item || null;
var items = getItems(testMap);
var itemSliceIndex = items.indexOf(lastJumpItem);
var itemIdentifierToAdd = items.slice(itemSliceIndex + 1).shift();
var itemToAdd = simplifiedTestMap.filter(function (row) {
return row.item === itemIdentifierToAdd;
}).shift();
if (itemToAdd) {
return self.addJump(itemToAdd.part, itemToAdd.section, itemToAdd.item).then(resolve);
} else {
return resolve();
}
});
},
/**
* Adds the next item to the end of the jump table
*
* @param {Object} params
* @returns {Promise}
*/
jumpToNextItem: function jumpToNextItem(params) {
var self = this;
addResponsesToResponseStore(params);
return new Promise(function (resolve) {
var simplifiedTestMap = getSimplifiedTestMap(testMap);
var lastJumpItem = self.getLastJump().item || null;
var items = getItems(testMap);
var itemSliceIndex = items.indexOf(lastJumpItem);
var itemIdentifierToAdd = items.slice(itemSliceIndex + 1).shift();
var itemToAdd = simplifiedTestMap.filter(function (row) {
return row.item === itemIdentifierToAdd;
}).shift();
var lastJumpItemData = simplifiedTestMap.filter(function (row) {
return row.item === lastJumpItem;
}).shift();
if (lastJumpItemData && lastJumpItemData.itemHasBranchRule) {
return itemStore.get(lastJumpItem).then(function (item) {
branchRule(lastJumpItemData.itemBranchRule, item, params, responseStore).then(function (itemIdentifierToAddd) {
if (itemIdentifierToAddd !== null) {
itemToAdd = simplifiedTestMap.filter(function (row) {
return row.item === itemIdentifierToAddd;
}).shift();
}
self.addJump(itemToAdd.part, itemToAdd.section, itemToAdd.item).then(resolve);
}).catch(function (err) {
return Promise.reject(err);
});
}).catch(function (err) {
return Promise.reject(err);
});
} else {
if (itemToAdd) {
return self.addJump(itemToAdd.part, itemToAdd.section, itemToAdd.item).then(resolve);
} else {
return resolve();
}
}
});
},
/**
* Adds the first item of the next section to the end of the jump table
*
* @returns {Promise}
*/
jumpToNextSection: function jumpToNextSection() {
var self = this;
return new Promise(function (resolve) {
var simplifiedTestMap = getSimplifiedTestMap(testMap);
var lastJumpSection = self.getLastJump().section || null;
var sections = getSections(testMap);
var sectionSliceIndex = sections.indexOf(lastJumpSection);
var sectionIdentifierToAdd = sections.slice(sectionSliceIndex + 1).shift();
var itemToAdd = simplifiedTestMap.filter(function (row) {
return row.section === sectionIdentifierToAdd;
}).shift();
if (itemToAdd) {
return self.addJump(itemToAdd.part, itemToAdd.section, itemToAdd.item).then(resolve);
} else {
return resolve();
}
});
},
/**
* Jumps to the previous item by deleting the last entry of the jump table.
*
* @returns {Promise}
*/
jumpToPreviousItem: function jumpToPreviousItem() {
return new Promise(function (resolve) {
jumpTable.pop();
resolve();
});
},
/**
* Returns the jump table.
*
* @returns {Jump[]}
*/
getJumpTable: function getJumpTable() {
return jumpTable;
},
/**
* Returns the last entry of the jump table which represent the current state of the navigation.
*
* @returns {Jump}
*/
getLastJump: function getLastJump() {
return jumpTable.length > 0 ? jumpTable[jumpTable.length - 1] : {};
}
};
};
return offlineJumpTableFactory;
});