tao-test/app/taoQtiTestPreviewer/views/js/previewer/proxy/test.js

275 lines
10 KiB
JavaScript

/**
* 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) 2020-2022 (original work) Open Assessment Technologies SA ;
*/
/**
* Test runner proxy for the QTI test previewer
*
* @author Hanna Dzmitryieva <hanna@taotesting.com>
*/
define([
'core/promiseQueue',
'core/request',
'util/url',
'taoQtiTest/runner/helpers/map'
], function (promiseQueue, request, urlUtil, mapHelper) {
'use strict';
const serviceControllerInit = 'TestPreviewer';
const serviceControllerGetItem = 'Previewer';
const serviceExtension = 'taoQtiTestPreviewer';
/**
* The possible states of the test session,
* coming from the test context
* (this state comes from the backend)
*/
const testSessionStates = Object.freeze({
initial: 0,
interacting: 1,
modalFeedback: 2,
suspended: 3,
closed: 4
});
/**
* The possible states of an item session,
* coming from the test context
* (this state comes from the backend)
*/
const itemSessionStates = Object.freeze({
initial: 0,
interacting: 1,
modalFeedback: 2,
suspended: 3,
closed: 4,
solution: 5,
review: 6
});
/**
* Updates testContext with item data
* @param {Object} testMap
* @param {Number} position - item position
* @param {Object} testContext
* @param {Array} presetMap
*/
function updateTestContextWithItem(testMap, position, testContext, presetMap) {
const jump = mapHelper.getJump(testMap, position);
const item = mapHelper.getItemAt(testMap, position);
if (!item) {
return;
}
testContext.testPartId = jump.part;
testContext.sectionId = jump.section;
testContext.itemIdentifier = jump.identifier;
testContext.itemSessionState = itemSessionStates.initial;
testContext.options = createContextOptions(item, presetMap);
}
/**
* Convert preset categories to context.options
* @param {Object} item
* @param {Array} presetMap
* @returns {Object} options
*/
function createContextOptions(item, presetMap) {
const options = {};
presetMap.forEach(category => {
const pluginId = Object.keys(category)[0];
const categoryId = category[pluginId];
if (item.categories.includes(categoryId)) {
options[pluginId] = true;
}
});
return options;
}
/**
* QTI proxy definition
* Related to remote services calls
* @type {Object}
*/
return {
name: 'qtiTestPreviewerProxy',
/**
* Installs the proxy
*/
install() {
/**
* A promise queue to ensure requests run sequentially
*/
this.queue = promiseQueue();
},
/**
* Initializes the proxy
* @param {Object} configs - configuration from proxy
* @param {String} configs.options.testUri - The identifier of the test
* @returns {Promise} - Returns a promise. The proxy will be fully initialized on resolve.
* Any error will be provided if rejected.
*/
init(configs) {
this.itemStore = {};
return request({
url: urlUtil.route('init', serviceControllerInit, serviceExtension),
data: { testUri: configs.options.testUri }
}).then(response => {
const data = response.data;
//the received map is not complete and should be "built"
this.builtTestMap = mapHelper.reindex(data.testMap);
this.presetMap = data.presetMap || [];
delete data.presetMap;
const firstJump = this.builtTestMap.jumps[0] || {};
const firstItem = mapHelper.getItemAt(this.builtTestMap, 0);
data.testContext = {
itemIdentifier: firstJump.identifier,
itemPosition: 0,
itemSessionState: 0,
testPartId: firstJump.part,
sectionId: firstJump.section,
canMoveBackward: true,
state: testSessionStates.interacting,
attempt: 1,
options: createContextOptions(firstItem, this.presetMap)
};
return data;
});
},
/**
* Uninstalls the proxy
* @returns {Promise} - Returns a promise. The proxy will be fully uninstalled on resolve.
* Any error will be provided if rejected.
*/
destroy() {
// no request, just a resources cleaning
this.queue = null;
if (this.itemStore) {
this.itemStore = null;
}
// the method must return a promise
return Promise.resolve();
},
/**
* Gets an item definition by its URI, also gets its current state
* @param {String} itemIdentifier - The URI of the item to get
* @returns {Promise} - Returns a promise. The item data will be provided on resolve.
* Any error will be provided if rejected.
*/
getItem(itemIdentifier) {
if (itemIdentifier in this.itemStore) {
// Load item from store
return Promise.resolve(this.itemStore[itemIdentifier]);
} else {
// Load from server; Store in store
const { uri } = mapHelper.getItem(this.builtTestMap, itemIdentifier) || {};
if (!uri) {
throw new Error(`There is no item ${itemIdentifier} in the testMap!`);
}
return request({
url: urlUtil.route('getItem', serviceControllerGetItem, serviceExtension),
data: { serviceCallId: 'previewer', itemUri: uri },
noToken: true
})
.then(data => {
data.itemData = data.content;
data.itemIdentifier = data.content.data.identifier;
data.itemState = {};
this.itemStore[itemIdentifier] = data;
return data;
});
}
},
/**
* Call action on the test
* @param {string} itemIdentifier - the current item
* @param {string} action - the action id
* @param {Object} params
* @returns {Promise} resolves with the response
*/
callItemAction(itemIdentifier, action, params = {}) {
const dataHolder = this.getDataHolder();
const testContext = dataHolder.get('testContext');
const testMap = dataHolder.get('testMap');
const actions = {
//simulate backend move action
move: () => {
if (params.direction === 'next') {
if (params.scope === 'testPart') {
const testPartPosition = testMap.parts[testContext.testPartId].position;
const nextPartsSorted = Object.values(testMap.parts)
.filter(p => p.position > testPartPosition)
.sort((a, b) => a.position - b.position);
if (nextPartsSorted.length === 0) {
testContext.state = testSessionStates.closed;
} else {
testContext.itemPosition = Math.min(
testMap.stats.total - 1,
nextPartsSorted[0].position
);
}
} else {
if (testContext.itemPosition + 1 >= testMap.stats.total) {
testContext.state = testSessionStates.closed;
} else {
testContext.itemPosition = Math.min(
testMap.stats.total - 1,
testContext.itemPosition + 1
);
}
}
}
if (params.direction === 'previous') {
testContext.itemPosition = Math.max(0, testContext.itemPosition - 1);
}
if (params.direction === 'jump' && params.ref >= 0) {
testContext.itemPosition = params.ref;
}
updateTestContextWithItem(testMap, testContext.itemPosition, testContext, this.presetMap);
return { testContext, testMap };
},
flagItem: () => Promise.resolve()
};
actions.skip = actions.move;
if (params.itemState) {
// store itemState in itemStore
this.itemStore[itemIdentifier].itemState = params.itemState;
}
if (typeof actions[action] === 'function') {
return actions[action]();
}
},
/**
* Calls an action related to the test
* @returns {Promise} - Returns a promise. The result of the request will be provided on resolve.
* Any error will be provided if rejected.
*/
callTestAction() {
// the method must return a promise
return Promise.resolve();
}
};
});