UNPKG

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

Version:
487 lines (463 loc) 19.3 kB
define(['lodash', 'i18n', 'core/format', 'taoQtiTest/runner/helpers/map'], function (_, __, format, mapHelper) { 'use strict'; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __; format = format && Object.prototype.hasOwnProperty.call(format, 'default') ? format['default'] : format; 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) 2018-2019 (original work) Open Assessment Technologies SA ; */ /** * @typedef {Object} progressDetails * @property {Number} position - the position in the current element * @property {Number} reached - the number of reached elements (at least one item viewed) * @property {Number} viewed - the number of viewed elements (all items viewed) * @property {Number} completed - the number of completed elements (all questions answered) * @property {Number} total - the total number of elements */ /** * @typedef {itemStats} progressData * @property {Number} position - the position in the scope * @property {Number} overallCompleted - the number of completed items in the test * @property {Number} overall - the total number of items in the test * @property {progressDetails} sections - the details of testSections in the scope * @property {progressDetails} parts - the details of testParts in the scope * @property {progressDetails} answerableSections - the details of testSections that contain questions in the scope * @property {progressDetails} answerableParts - the details of testParts that contain questions in the scope * @property {progressDetails} matchedCategories - the details of items that match the expected categories in the scope */ /** * @typedef {Object} progressIndicator * @property {Number} position - the position in the scope * @property {Number} total - the length of the scope * @property {Number} ratio - the progress ratio of the indicator * @property {String} label - the text to display for the indicator */ /** * @typedef {Object} progressConfig * @property {String} scope - the scope of the progression * @property {String} indicator - the type of progression * @property {Bool} showTotal - display 'item x of y' (true) | 'item x' * @property {Array} categories - categories to count by them */ /** * Default progress config * @type {Object} */ var defaultConfig = { scope: 'test', indicator: 'percentage', showTotal: true, categories: [] }; /** * List of labels by types * @type {Object} */ var labels = { item: { long: __('Item %d of %d'), short: __('Item %d') }, section: { long: __('Section %d of %d'), short: __('Section %d') } }; /** * Simple map of progress stats computers * @type {Object} */ var scopes = { /** * Gets stats for the whole test * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressData} */ test: function test(testMap, testContext, config) { var stats = getProgressStats(testMap, testContext, config, 'test'); var item = mapHelper.getItemAt(testMap, testContext.itemPosition); stats.position = item.position + 1; return stats; }, /** * Gets stats for the current test part * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressData} */ testPart: function testPart(testMap, testContext, config) { var stats = getProgressStats(testMap, testContext, config, 'testPart'); var item = mapHelper.getItemAt(testMap, testContext.itemPosition); stats.position = item.positionInPart + 1; return stats; }, /** * Gets stats for the current test section * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressData} */ testSection: function testSection(testMap, testContext, config) { var stats = getProgressStats(testMap, testContext, config, 'testSection'); var item = mapHelper.getItemAt(testMap, testContext.itemPosition); stats.position = item.positionInSection + 1; return stats; } }; /** * Simple map of progress indicator computers * @type {Object} */ var indicators = { /** * Indicator that shows the percentage of completed items * @param {progressData} stats * @returns {progressIndicator} */ percentage: function percentage(stats) { return getRatioProgression(stats.answered, stats.questions); }, /** * Indicator that shows the position of current item * @param {progressData} stats * @param {progressConfig} config * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressIndicator} */ position: function position(stats, config) { return getPositionProgression(stats.position, stats.total, 'item', config); }, /** * Indicator that shows the number of viewed questions * @param {progressData} stats * @param {progressConfig} config * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressIndicator} */ questions: function questions(stats, config) { return getPositionProgression(stats.questionsViewed, stats.questions, 'item', config); }, /** * Indicator that shows the number of reached answerable sections * @param {progressData} stats * @param {progressConfig} config * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressIndicator} */ sections: function sections(stats, config) { return getPositionProgression(stats.answerableSections.reached, stats.answerableSections.total, 'section', config); }, /** * Indicator that shows the number of viewed items which have categories from the configuration * (show all if categories are not set) * @param {progressData} stats * @param {progressConfig} config * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them */ categories: function categories(stats, config) { return getPositionProgression(stats.matchedCategories.position, stats.matchedCategories.total, 'item', config); } }; /** * Fix the test map if the current test part is linear, as the current item should not be answered. * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @returns {Object} The fixed test map */ function getFixedMap(testMap, testContext) { const currentTestPart = mapHelper.getPart(testMap, testContext.testPartId); const currentItem = mapHelper.getItemAt(testMap, testContext.itemPosition); if (currentItem.answered && currentTestPart.isLinear) { const fixedTestMap = _.cloneDeep(testMap); const fixedCurrentItem = mapHelper.getItemAt(fixedTestMap, testContext.itemPosition); fixedCurrentItem.answered = false; return fixedTestMap; } return testMap; } /** * Gets an empty stats record * @returns {progressDetails} */ function getEmptyStats() { return { position: 0, reached: 0, viewed: 0, completed: 0, total: 0 }; } /** * Updates the progress stats from the given element * @param {progressDetails} stats - The stats details to update * @param {Object} element - The element from which take the details * @param {Number} position - The current item position */ function updateStats(stats, element, position) { if (element.position <= position) { stats.position++; } if (element.stats.viewed) { stats.reached++; if (element.stats.viewed === element.stats.total) { stats.viewed++; } } if (element.stats.answered) { if (element.stats.answered === element.stats.questions) { stats.completed++; } } stats.total++; } /** * Updates the progress stats from the given element * @param {progressDetails} stats - The stats details to update * @param {Object} element - The element from which take the details * @param {Number} position - The current item position */ function updateItemStats(stats, element, position) { if (element.position <= position) { stats.position++; } if (element.viewed) { stats.reached++; stats.viewed++; } if (element.answered) { stats.completed++; } stats.total++; } /** * Convert list of the categories to the hashtable to improve performance * @param categories * @returns {*} */ function getCategoriesToMatch(categories) { var matchSize = categories && categories.length; return matchSize && _.reduce(categories, function (map, category) { map[category] = true; return map; }, {}); } /** * Completes the progression stats * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @param {String} [scope] - The name of the scope. Can be: test, part, section (default: test) * @returns {progressData} */ function getProgressStats(testMap, testContext, config, scope) { var fixedMap = getFixedMap(testMap, testContext); var scopedMap = mapHelper.getScopeMap(fixedMap, testContext.itemPosition, scope); var stats = _.clone(scopedMap.stats); var categoriesToMatch; var matchSize; if (config.indicator === 'categories') { categoriesToMatch = getCategoriesToMatch(config.categories); matchSize = config.categories && config.categories.length; stats.matchedCategories = getEmptyStats(); } stats.parts = getEmptyStats(); stats.sections = getEmptyStats(); stats.answerableParts = getEmptyStats(); stats.answerableSections = getEmptyStats(); _.forEach(scopedMap.parts, function (part) { updateStats(stats.parts, part, testContext.itemPosition); if (part.stats.questions > 0) { updateStats(stats.answerableParts, part, testContext.itemPosition); } _.forEach(part.sections, function (section) { updateStats(stats.sections, section, testContext.itemPosition); if (section.stats.questions > 0) { updateStats(stats.answerableSections, section, testContext.itemPosition); } if (config.indicator === 'categories') { _.forEach(section.items, function (item) { if (matchCategories(item.categories, categoriesToMatch, matchSize)) { updateItemStats(stats.matchedCategories, item, testContext.itemPosition); } }); } }); }); return stats; } /** * * @param {Array} categories - List of categories to check * @param {Object} expectedCategories - Hashtable of expected categories * @param {Number} minWanted - Minimal number of expected categories that should match * @returns {Boolean} */ function matchCategories(categories, expectedCategories, minWanted) { var matched = 0; if (expectedCategories) { _.forEach(categories, function (category) { if (expectedCategories[category]) { matched++; if (matched >= minWanted) { return false; } } }); } return matched === minWanted; } /** * Gets the progression ratio * @param {Number} position * @param {Number} total * @returns {Number} */ function getRatio(position, total) { if (position && total > 0) { return Math.floor(position / total * 100); } return 0; } /** * Gets the label of the progress bar for an item * @param {Number} position - the current position * @param {Number} total - the total number of items * @param {String} type - the type of element that is represented * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {String} */ function getProgressionLabel(position, total, type, config) { var patterns = labels[type] || labels.item; var pattern = config.showTotal ? patterns.long : patterns.short; return format(pattern, position || '0', total || '0'); } /** * Gets the progression based on position * @param {Number} position - the current position * @param {Number} total - the total number of items * @param {String} type - the type of element that is represented * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressIndicator} */ function getPositionProgression(position, total, type, config) { return { position: position || 0, total: total || 0, ratio: getRatio(position, total), label: getProgressionLabel(position, total, type, config) }; } /** * Gets the progression based on a ratio * @param {Number} position - the current position * @param {Number} total - the total number of items * @returns {progressIndicator} */ function getRatioProgression(position, total) { var ratio = getRatio(position, total); return { position: position || 0, total: total || 0, ratio: ratio, label: `${ratio}%` }; } var progress = { /** * Checks that categories matched * @param categories * @param expectedCategories * @returns {Boolean} */ isMatchedCategories: function validCategories(categories, expectedCategories) { var categoriesToMatch = getCategoriesToMatch(expectedCategories); var matchSize = expectedCategories && expectedCategories.length; return matchCategories(categories, categoriesToMatch, matchSize); }, /** * Computes the progress stats for the specified scope * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config - a config object * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @returns {progressData} */ computeStats(testMap, testContext, config) { const testPart = mapHelper.getPart(testMap, testContext.testPartId); const statsComputer = config.scope && scopes[config.scope] || scopes.test; const stats = statsComputer(testMap, testContext, config || defaultConfig); stats.overall = testMap.stats.total; if (testPart && testPart.isLinear) { stats.overallCompleted = testMap.stats.answered - 1; } else { stats.overallCompleted = testMap.stats.answered; } return stats; }, /** * Computes the specified progress indicator * @param {progressData} stats - the progress stats * @param {String} type - the [type="percentage"] of indicator to compute (could be: percentage, position, questions, sections) * @param {progressConfig} [config] - a config object * @param {Boolean} [config.showTotal] - display 'item x of y' (true) | 'item x' * @returns {progressIndicator} */ computeIndicator: function computeIndicator(stats, type, config) { var indicatorComputer = type && indicators[type] || indicators.percentage; return indicatorComputer(stats || {}, config || defaultConfig); }, /** * * @param {Object} testMap - the actual test map * @param {Object} testContext - the actual test context * @param {progressConfig} config - a config object * @param {String} config.indicator - the type of progression * @param {String} config.scope - the scope of the progression * @param {Array} config.categories - categories to count by them * @param {Boolean} [config.showTotal=true] - display 'item x of y' (true) | 'item x' */ computeProgress: function computeProgress(testMap, testContext, config) { var progressData; config = _.defaults(config || {}, defaultConfig); progressData = this.computeStats(testMap, testContext, config); return this.computeIndicator(progressData, config.indicator, config); } }; return progress; });