@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
218 lines (200 loc) • 9.77 kB
JavaScript
define(['lodash', 'i18n', 'moment', 'core/format', 'core/logger'], function (_, __, moment, format, loggerFactory) { 'use strict';
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
__ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
moment = moment && Object.prototype.hasOwnProperty.call(moment, 'default') ? moment['default'] : moment;
format = format && Object.prototype.hasOwnProperty.call(format, 'default') ? format['default'] : format;
loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory;
/**
* 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) 2018 (original work) Open Assessment Technologies SA ;
*/
var logger = loggerFactory('taoQtiTest/runner/plugins/controls/timer/timers');
/**
* We receive values in seconds, so we convert them to milliseconds
*/
var precision = 1000;
/**
* The timer's scope
*/
var scopes = ['item', 'section', 'testPart', 'test'];
/**
* Map qti class names to scopes
*/
var scopeMapping = {
assessmentTest: 'test',
assessmentSection: 'section',
assessmentItemRef: 'item'
};
/**
* helps you get the scope from a scope or qti class name
* @param {String} value - scope or qti class name
* @returns {String?} the scope
*/
var getScope = function getScope(value) {
if (scopeMapping[value]) {
return scopeMapping[value];
}
if (scopes.includes(value)) {
return value;
}
return null;
};
/**
* The text of warning messages
* TODO add warning messages for other timers types
*/
var warningMessages = {
item: __('Warning – You have %s remaining to complete this item.'),
section: __('Warning – You have %s remaining to complete this section.'),
testPart: __('Warning – You have %s remaining to complete this test part.'),
test: __('Warning – You have %s remaining to complete this test.')
};
/**
* The text of warning messages for screenreader
*/
const warningMessagesForScreenraeder = {
item: __('You have %s remaining to complete the current item.'),
section: __('You have %s left to answer remaining %s questions.'),
testPart: __('You have %s left to answer remaining %s questions.'),
test: __('You have %s left to answer remaining %s questions.')
};
/**
* Get the timers objects from the time constraints andt the given config
* @param {Object[]} timeConstraints - as defined in the testContext
* @param {Boolean} isLinear - is the current navigation mode linear
* @param {Object} [config] - timers config
* @param {Object[]} [config.warnings] - the warnings to apply to the timers (max only for now)
* @param {Object[]} [config.warnings] - the warnings to apply to the timers (max only for now)
* @returns {timer[]} the timers
*/
function getTimers(timeConstraints, isLinear, config) {
var timers = {};
/**
* The warnings comes in a weird format (ie. {scope:{threshold:level}}) , so we reformat them
*/
var constraintsWarnings = _.reduce(config.warnings, function (acc, warnings, qtiScope) {
var scope = getScope(qtiScope);
acc[scope] = _.map(warnings, function (value, key) {
return {
threshold: parseInt(key, 10) * precision,
message: function applyMessage(remainingTime) {
var displayRemaining = moment.duration(remainingTime / precision, 'seconds').humanize();
return format(warningMessages[scope], displayRemaining);
},
level: value,
shown: false
};
});
return acc;
}, {});
/**
* The warnings comes in a weird format (ie. {scope:[threshold, ...]}) , so we reformat them
*/
const constraintsWarningsForScreenreader = _.reduce(config.warningsForScreenreader, (acc, warnings, qtiScope) => {
const scope = getScope(qtiScope);
acc[scope] = _.map(warnings, value => ({
threshold: parseInt(value, 10) * precision,
message: function applyMessage(remainingTime, unansweredQuestions) {
const displayRemaining = moment.duration(remainingTime / precision, 'seconds').humanize();
return format(warningMessagesForScreenraeder[scope], displayRemaining, unansweredQuestions);
},
scope,
shown: false
}));
return acc;
}, {});
/**
* Build a timer of a given type from a time constraints
* @param {String} type - min, max, locked
* @param {Object} constraintData
* @returns {timer} timer
*/
var buildTimer = function buildTimer(type, constraintData) {
/**
* @typedef {Object} timer
* @property {String} id - identify the timer (for max, it's the source for backward compat)
* @property {String} type - min, max or locked
* @property {String} label - the title to display
* @property {String} scope - the timer's scope (item, section, etc.)
* @property {String} qtiClassName - the QTI class of the timers applies to
* @property {String} source - the ID of the element the timers belongs to
* @property {Number} extraTime - additional time data, object
* @property {Number} originalTime - the starting value of the timer, never changes, in ms.
* @property {Number} remainingTime - current value, in ms.
* @property {Number} remainingWithoutExtraTime - remaining time without extra time, in ms.
* @property {Number} total - total time (original time + extra time), in ms.
*/
var timer = _.pick(constraintData, ['label', 'scope', 'source', 'extraTime', 'qtiClassName']);
timer.type = type;
timer.allowLateSubmission = constraintData.allowLateSubmission;
if (type === 'min') {
timer.id = `${type}-${constraintData.scope}-${constraintData.source}`;
timer.originalTime = constraintData.minTime * precision;
timer.remainingTime = constraintData.minTimeRemaining * precision;
} else {
timer.id = constraintData.source;
timer.originalTime = constraintData.maxTime * precision;
timer.remainingTime = constraintData.maxTimeRemaining * precision;
}
timer.remainingWithoutExtraTime = timer.remainingTime;
if (timer.extraTime && timer.type !== 'min') {
timer.extraTime.consumed = timer.extraTime.consumed * precision;
timer.extraTime.remaining = timer.extraTime.remaining * precision;
timer.extraTime.total = timer.extraTime.total * precision;
timer.total = timer.originalTime + timer.extraTime.total;
timer.remainingTime += timer.extraTime.remaining;
}
//TODO supports warnings for other types
if (type === 'max' && _.isArray(constraintsWarnings[timer.scope])) {
timer.warnings = constraintsWarnings[timer.scope];
}
if (_.isArray(constraintsWarningsForScreenreader[timer.scope])) {
timer.warningsForScreenreader = constraintsWarningsForScreenreader[timer.scope];
}
const stats = config.questionsStats[timer.scope];
timer.unansweredQuestions = stats && stats.questions - stats.answered;
return timer;
};
_.forEach(timeConstraints, function (timeConstraint) {
var constraintData = _.clone(timeConstraint);
var newTimer;
constraintData.scope = getScope(timeConstraint.scope || timeConstraint.qtiClassName);
if (!constraintData.scope) {
logger.warn('Wrong data, a time constraint should apply to a valid scope, skipping');
} else if (constraintData.minTime === false && constraintData.maxTime === false) {
logger.warn('Time constraint defined with no time, skipping');
// minTime = maxTime -> one locked timer
} else if (config.guidedNavigation && isLinear && constraintData.maxTime && constraintData.minTime && constraintData.minTime === constraintData.maxTime && constraintData.maxTime > 0) {
newTimer = buildTimer('locked', constraintData);
timers[newTimer.id] = newTimer;
} else {
//minTime -> min timer
if (isLinear && constraintData.minTime && constraintData.minTime > 0) {
newTimer = buildTimer('min', constraintData);
timers[newTimer.id] = newTimer;
}
//maxTime -> max timer
if (constraintData.maxTime && constraintData.maxTime > 0) {
newTimer = buildTimer('max', constraintData);
timers[newTimer.id] = newTimer;
}
}
});
logger.debug('Timers built from timeConstraints', timers);
return timers;
}
return getTimers;
});