1391 lines
54 KiB
JavaScript
1391 lines
54 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) 2015 (original work) Open Assessment Technologies SA;
|
|||
|
*
|
|||
|
*/
|
|||
|
|
|||
|
define([
|
|||
|
'jquery',
|
|||
|
'lodash',
|
|||
|
'i18n',
|
|||
|
'module',
|
|||
|
'taoQtiTest/testRunner/actionBarTools',
|
|||
|
'taoQtiTest/testRunner/testReview',
|
|||
|
'taoQtiTest/testRunner/progressUpdater',
|
|||
|
'taoQtiTest/testRunner/testMetaData',
|
|||
|
'serviceApi/ServiceApi',
|
|||
|
'serviceApi/UserInfoService',
|
|||
|
'serviceApi/StateStorage',
|
|||
|
'iframeNotifier',
|
|||
|
'mathJax',
|
|||
|
'ui/feedback',
|
|||
|
'ui/deleter',
|
|||
|
'moment',
|
|||
|
'ui/modal',
|
|||
|
'ui/progressbar'
|
|||
|
],
|
|||
|
function (
|
|||
|
$,
|
|||
|
_,
|
|||
|
__,
|
|||
|
module,
|
|||
|
actionBarTools,
|
|||
|
testReview,
|
|||
|
progressUpdater,
|
|||
|
testMetaDataFactory,
|
|||
|
ServiceApi,
|
|||
|
UserInfoService,
|
|||
|
StateStorage,
|
|||
|
iframeNotifier,
|
|||
|
MathJax,
|
|||
|
feedback,
|
|||
|
deleter,
|
|||
|
moment,
|
|||
|
modal
|
|||
|
) {
|
|||
|
|
|||
|
'use strict';
|
|||
|
|
|||
|
var timerIds = [],
|
|||
|
currentTimes = [],
|
|||
|
lastDates = [],
|
|||
|
timeDiffs = [],
|
|||
|
waitingTime = 0,
|
|||
|
$timers,
|
|||
|
$controls,
|
|||
|
timerIndex,
|
|||
|
testMetaData,
|
|||
|
sessionStateService,
|
|||
|
$doc = $(document),
|
|||
|
optionNextSection = 'x-tao-option-nextSection',
|
|||
|
optionNextSectionWarning = 'x-tao-option-nextSectionWarning',
|
|||
|
optionReviewScreen = 'x-tao-option-reviewScreen',
|
|||
|
optionEndTestWarning = 'x-tao-option-endTestWarning',
|
|||
|
optionNoExitTimedSectionWarning = 'x-tao-option-noExitTimedSectionWarning',
|
|||
|
TestRunner = {
|
|||
|
// Constants
|
|||
|
'TEST_STATE_INITIAL': 0,
|
|||
|
'TEST_STATE_INTERACTING': 1,
|
|||
|
'TEST_STATE_MODAL_FEEDBACK': 2,
|
|||
|
'TEST_STATE_SUSPENDED': 3,
|
|||
|
'TEST_STATE_CLOSED': 4,
|
|||
|
'TEST_NAVIGATION_LINEAR': 0,
|
|||
|
'TEST_NAVIGATION_NONLINEAR': 1,
|
|||
|
'TEST_ITEM_STATE_INTERACTING': 1,
|
|||
|
|
|||
|
/**
|
|||
|
* Prepare a transition to another item
|
|||
|
* @param {Function} [callback]
|
|||
|
*/
|
|||
|
beforeTransition: function (callback) {
|
|||
|
// Ask the top window to start the loader.
|
|||
|
iframeNotifier.parent('loading');
|
|||
|
|
|||
|
// Disable buttons.
|
|||
|
this.disableGui();
|
|||
|
|
|||
|
$controls.$itemFrame.hide();
|
|||
|
$controls.$rubricBlocks.hide();
|
|||
|
$controls.$timerWrapper.hide();
|
|||
|
|
|||
|
// Wait at least waitingTime ms for a better user experience.
|
|||
|
if (typeof callback === 'function') {
|
|||
|
setTimeout(callback, waitingTime);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Complete a transition to another item
|
|||
|
*/
|
|||
|
afterTransition: function () {
|
|||
|
this.enableGui();
|
|||
|
|
|||
|
//ask the top window to stop the loader
|
|||
|
iframeNotifier.parent('unloading');
|
|||
|
testMetaData.addData({
|
|||
|
'ITEM' : {'ITEM_START_TIME_CLIENT' : Date.now() / 1000}
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Jumps to a particular item in the test
|
|||
|
* @param {Number} position The position of the item within the test
|
|||
|
*/
|
|||
|
jump: function(position) {
|
|||
|
var self = this,
|
|||
|
action = 'jump',
|
|||
|
params = {position: position};
|
|||
|
this.disableGui();
|
|||
|
|
|||
|
if( this.isJumpOutOfSection(position) && this.isCurrentItemActive() && this.isTimedSection() ){
|
|||
|
this.exitTimedSection(action, params);
|
|||
|
} else {
|
|||
|
this.killItemSession(function() {
|
|||
|
self.actionCall(action, params);
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Push to server how long user seen that item before to track duration
|
|||
|
* @param {Number} duration
|
|||
|
*/
|
|||
|
keepItemTimed: function(duration){
|
|||
|
if (duration) {
|
|||
|
var self = this,
|
|||
|
action = 'keepItemTimed',
|
|||
|
params = {duration: duration};
|
|||
|
self.actionCall(action, params);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Marks an item for later review
|
|||
|
* @param {Boolean} flag The state of the flag
|
|||
|
* @param {Number} position The position of the item within the test
|
|||
|
*/
|
|||
|
markForReview: function(flag, position) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
// Ask the top window to start the loader.
|
|||
|
iframeNotifier.parent('loading');
|
|||
|
|
|||
|
// Disable buttons.
|
|||
|
this.disableGui();
|
|||
|
|
|||
|
$.ajax({
|
|||
|
url: self.testContext.markForReviewUrl,
|
|||
|
cache: false,
|
|||
|
async: true,
|
|||
|
type: 'POST',
|
|||
|
dataType: 'json',
|
|||
|
data: {
|
|||
|
flag: flag,
|
|||
|
position: position
|
|||
|
},
|
|||
|
success: function(data) {
|
|||
|
// update the item flagged state
|
|||
|
if (self.testReview) {
|
|||
|
self.testReview.setItemFlag(position, flag);
|
|||
|
self.testReview.updateNumberFlagged(self.testContext, position, flag);
|
|||
|
if (self.testContext.itemPosition === position) {
|
|||
|
self.testContext.itemFlagged = flag;
|
|||
|
}
|
|||
|
self.updateTools(self.testContext);
|
|||
|
}
|
|||
|
|
|||
|
// Enable buttons.
|
|||
|
self.enableGui();
|
|||
|
|
|||
|
//ask the top window to stop the loader
|
|||
|
iframeNotifier.parent('unloading');
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Move to the next available item
|
|||
|
*/
|
|||
|
moveForward: function () {
|
|||
|
var self = this,
|
|||
|
action = 'moveForward';
|
|||
|
|
|||
|
function doExitSection() {
|
|||
|
if( self.isTimedSection() && !self.testContext.isTimeout){
|
|||
|
self.exitTimedSection(action);
|
|||
|
} else {
|
|||
|
self.exitSection(action);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
this.disableGui();
|
|||
|
|
|||
|
if( (( this.testContext.numberItemsSection - this.testContext.itemPositionSection - 1) == 0) && this.isCurrentItemActive()){
|
|||
|
if (this.shouldDisplayEndTestWarning()) {
|
|||
|
this.displayEndTestWarning(doExitSection);
|
|||
|
this.enableGui();
|
|||
|
} else {
|
|||
|
doExitSection();
|
|||
|
}
|
|||
|
|
|||
|
} else {
|
|||
|
this.killItemSession(function () {
|
|||
|
self.actionCall(action);
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Check if necessary to display an end test warning
|
|||
|
*/
|
|||
|
shouldDisplayEndTestWarning: function(){
|
|||
|
return (this.testContext.isLast === true && this.hasOption(optionEndTestWarning));
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Warns upon exiting test
|
|||
|
*/
|
|||
|
displayEndTestWarning: function(nextAction){
|
|||
|
var options = {
|
|||
|
confirmLabel: __('OK'),
|
|||
|
cancelLabel: __('Cancel'),
|
|||
|
showItemCount: false
|
|||
|
};
|
|||
|
|
|||
|
this.displayExitMessage(
|
|||
|
__('You are about to submit the test. You will not be able to access this test once submitted. Click OK to continue and submit the test.'),
|
|||
|
nextAction,
|
|||
|
options
|
|||
|
);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Move to the previous available item
|
|||
|
*/
|
|||
|
moveBackward: function () {
|
|||
|
var self = this,
|
|||
|
action = 'moveBackward';
|
|||
|
|
|||
|
this.disableGui();
|
|||
|
|
|||
|
if( (this.testContext.itemPositionSection == 0) && this.isCurrentItemActive() && this.isTimedSection() ){
|
|||
|
this.exitTimedSection(action);
|
|||
|
} else {
|
|||
|
this.killItemSession(function () {
|
|||
|
self.actionCall(action);
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Checks if a position is out of the current section
|
|||
|
* @param {Number} jumpPosition
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
isJumpOutOfSection: function(jumpPosition){
|
|||
|
var items = this.getCurrentSectionItems(),
|
|||
|
isJumpToOtherSection = true,
|
|||
|
isValidPosition = (jumpPosition >= 0) && ( jumpPosition < this.testContext.numberItems );
|
|||
|
|
|||
|
if( isValidPosition){
|
|||
|
for(var i in items ) {
|
|||
|
if (!items.hasOwnProperty(i)) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
if( items[i].position == jumpPosition ){
|
|||
|
isJumpToOtherSection = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
isJumpToOtherSection = false;
|
|||
|
}
|
|||
|
|
|||
|
return isJumpToOtherSection;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Exit from the current section. Set the exit code.de
|
|||
|
* @param {String} action
|
|||
|
* @param {Object} params
|
|||
|
* @param {Number} [exitCode]
|
|||
|
*/
|
|||
|
exitSection: function(action, params, exitCode){
|
|||
|
var self = this;
|
|||
|
|
|||
|
testMetaData.addData({"SECTION" : {"SECTION_EXIT_CODE" : exitCode || testMetaData.SECTION_EXIT_CODE.COMPLETED_NORMALLY}});
|
|||
|
self.killItemSession(function () {
|
|||
|
self.actionCall(action, params);
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Tries to exit a timed section. Display a confirm message.
|
|||
|
* @param {String} action
|
|||
|
* @param {Object} params
|
|||
|
*/
|
|||
|
exitTimedSection: function(action, params){
|
|||
|
var self = this,
|
|||
|
qtiRunner = this.getQtiRunner(),
|
|||
|
doExitTimedSection = function() {
|
|||
|
if (qtiRunner) {
|
|||
|
qtiRunner.updateItemApi();
|
|||
|
}
|
|||
|
self.exitSection(action, params);
|
|||
|
};
|
|||
|
|
|||
|
if ((action === 'moveForward' && this.shouldDisplayEndTestWarning()) // prevent duplicate warning
|
|||
|
|| this.hasOption(optionNoExitTimedSectionWarning) // check if warning is disabled
|
|||
|
|| this.testContext.keepTimerUpToTimeout // no need to display the message as we may be able to go back
|
|||
|
) {
|
|||
|
doExitTimedSection();
|
|||
|
} else {
|
|||
|
this.displayExitMessage(
|
|||
|
__('After you complete the section it would be impossible to return to this section to make changes. Are you sure you want to end the section?'),
|
|||
|
doExitTimedSection,
|
|||
|
{ scope: 'testSection' }
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
this.enableGui();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Tries to leave the current section and go to the next
|
|||
|
*/
|
|||
|
nextSection: function(){
|
|||
|
var self = this;
|
|||
|
var qtiRunner = this.getQtiRunner();
|
|||
|
var doNextSection = function() {
|
|||
|
self.exitSection('nextSection', null, testMetaData.SECTION_EXIT_CODE.QUIT);
|
|||
|
};
|
|||
|
|
|||
|
if (qtiRunner) {
|
|||
|
qtiRunner.updateItemApi();
|
|||
|
}
|
|||
|
|
|||
|
if (this.hasOption(optionNextSectionWarning)) {
|
|||
|
this.displayExitMessage(
|
|||
|
__('After you complete the section it would be impossible to return to this section to make changes. Are you sure you want to end the section?'),
|
|||
|
doNextSection,
|
|||
|
{ scope: 'testSection' }
|
|||
|
);
|
|||
|
} else {
|
|||
|
doNextSection();
|
|||
|
}
|
|||
|
|
|||
|
this.enableGui();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Gets the current progression within a particular scope
|
|||
|
* @param {String} [scope]
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
getProgression: function(scope) {
|
|||
|
var scopeSuffixMap = {
|
|||
|
test : '',
|
|||
|
testPart : 'Part',
|
|||
|
testSection : 'Section'
|
|||
|
};
|
|||
|
var scopeSuffix = scope && scopeSuffixMap[scope] || '';
|
|||
|
|
|||
|
return {
|
|||
|
total : this.testContext['numberItems' + scopeSuffix] || 0,
|
|||
|
answered : this.testContext['numberCompleted' + scopeSuffix] || 0,
|
|||
|
viewed : this.testContext['numberPresented' + scopeSuffix] || 0,
|
|||
|
flagged : this.testContext['numberFlagged' + scopeSuffix] || 0
|
|||
|
};
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Displays an exit message for a particular scope
|
|||
|
* @param {String} message
|
|||
|
* @param {Function} [action]
|
|||
|
* @param {Object} [options]
|
|||
|
* @param {String} [options.scope]
|
|||
|
* @param {String} [options.confirmLabel] - label of confirm button
|
|||
|
* @param {String} [options.cancelLabel] - label of cancel button
|
|||
|
* @param {Boolean} [options.showItemCount] - display the number of unanswered / flagged items in modal
|
|||
|
* @returns {jQuery} Returns the message box
|
|||
|
*/
|
|||
|
displayExitMessage: function(message, action, options) {
|
|||
|
var self = this,
|
|||
|
options = options || {},
|
|||
|
scope = options.scope,
|
|||
|
confirmLabel = options.confirmLabel || __('Yes'),
|
|||
|
cancelLabel = options.cancelLabel || __('No'),
|
|||
|
showItemCount = typeof options.showItemCount !== 'undefined' ? options.showItemCount : true;
|
|||
|
|
|||
|
var $confirmBox = $('.exit-modal-feedback');
|
|||
|
var progression = this.getProgression(scope);
|
|||
|
var unansweredCount = (progression.total - progression.answered);
|
|||
|
var flaggedCount = progression.flagged;
|
|||
|
|
|||
|
if (showItemCount) {
|
|||
|
if (unansweredCount && this.isCurrentItemAnswered()) {
|
|||
|
unansweredCount--;
|
|||
|
}
|
|||
|
|
|||
|
if (flaggedCount && unansweredCount) {
|
|||
|
message = __('You have %s unanswered question(s) and have %s item(s) marked for review.',
|
|||
|
unansweredCount.toString(),
|
|||
|
flaggedCount.toString()
|
|||
|
) + ' ' + message;
|
|||
|
} else {
|
|||
|
if (flaggedCount) {
|
|||
|
message = __('You have %s item(s) marked for review.', flaggedCount.toString()) + ' ' + message;
|
|||
|
}
|
|||
|
|
|||
|
if (unansweredCount) {
|
|||
|
message = __('You have %s unanswered question(s).', unansweredCount.toString()) + ' ' + message;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$confirmBox.find('.message').html(message);
|
|||
|
$confirmBox.find('.js-exit-confirm').html(confirmLabel);
|
|||
|
$confirmBox.find('.js-exit-cancel').html(cancelLabel);
|
|||
|
$confirmBox.modal({ width: 500 });
|
|||
|
|
|||
|
$confirmBox.find('.js-exit-cancel, .modal-close').off('click').on('click', function () {
|
|||
|
$confirmBox.modal('close');
|
|||
|
});
|
|||
|
|
|||
|
$confirmBox.find('.js-exit-confirm').off('click').on('click', function () {
|
|||
|
$confirmBox.modal('close');
|
|||
|
if (_.isFunction(action)) {
|
|||
|
action.call(self);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
return $confirmBox;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Kill current item section and execute callback function given as first parameter.
|
|||
|
* Item end execution time will be stored in metadata object to be sent to the server.
|
|||
|
* @param {function} callback
|
|||
|
*/
|
|||
|
killItemSession : function (callback) {
|
|||
|
testMetaData.addData({
|
|||
|
'ITEM' : {
|
|||
|
'ITEM_END_TIME_CLIENT' : Date.now() / 1000,
|
|||
|
'ITEM_TIMEZONE' : moment().utcOffset(moment().utcOffset()).format('Z')
|
|||
|
}
|
|||
|
});
|
|||
|
if (typeof callback !== 'function') {
|
|||
|
callback = _.noop;
|
|||
|
}
|
|||
|
this.itemServiceApi.kill(callback);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Checks if the current item is active
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
isCurrentItemActive: function(){
|
|||
|
return (this.testContext.itemSessionState !=4);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Tells is the current item has been answered or not
|
|||
|
* The item is considered answered when at least one response has been set to not empty {base : null}
|
|||
|
*
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
isCurrentItemAnswered: function(){
|
|||
|
var answered = false;
|
|||
|
_.each(this.getCurrentItemState(), function(state){
|
|||
|
if(state && _.isObject(state.response) && state.response.base !== null){
|
|||
|
answered = true;//at least one response is not null so consider the item answered
|
|||
|
return false;
|
|||
|
}
|
|||
|
});
|
|||
|
return answered;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Checks if a particular option is enabled for the current item
|
|||
|
* @param {String} option
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
hasOption: function(option) {
|
|||
|
return _.indexOf(this.testContext.categories, option) >= 0;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Gets access to the qtiRunner instance
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
getQtiRunner: function(){
|
|||
|
var itemFrame = document.getElementById('qti-item');
|
|||
|
var itemWindow = itemFrame && itemFrame.contentWindow;
|
|||
|
var itemContainerFrame = itemWindow && itemWindow.document.getElementById('item-container');
|
|||
|
var itemContainerWindow = itemContainerFrame && itemContainerFrame.contentWindow;
|
|||
|
return itemContainerWindow && itemContainerWindow.qtiRunner;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Checks if the current section is timed
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
isTimedSection: function(){
|
|||
|
var timeConstraints = this.testContext.timeConstraints,
|
|||
|
isTimedSection = false;
|
|||
|
for( var index in timeConstraints ){
|
|||
|
if(timeConstraints.hasOwnProperty(index) &&
|
|||
|
timeConstraints[index].qtiClassName === 'assessmentSection' ){
|
|||
|
isTimedSection = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return isTimedSection;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Gets the list of items owned by the current section
|
|||
|
* @returns {Array}
|
|||
|
*/
|
|||
|
getCurrentSectionItems: function(){
|
|||
|
var partId = this.testContext.testPartId,
|
|||
|
navMap = this.testContext.navigatorMap,
|
|||
|
sectionItems;
|
|||
|
|
|||
|
for( var partIndex in navMap ){
|
|||
|
if( !navMap.hasOwnProperty(partIndex)){
|
|||
|
continue;
|
|||
|
}
|
|||
|
if( navMap[partIndex].id !== partId ){
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
for(var sectionIndex in navMap[partIndex].sections){
|
|||
|
if( !navMap[partIndex].sections.hasOwnProperty(sectionIndex)){
|
|||
|
continue;
|
|||
|
}
|
|||
|
if( navMap[partIndex].sections[sectionIndex].active === true ){
|
|||
|
sectionItems = navMap[partIndex].sections[sectionIndex].items;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return sectionItems;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Skips the current item
|
|||
|
*/
|
|||
|
skip: function () {
|
|||
|
var self = this,
|
|||
|
doSkip = function() {
|
|||
|
self.disableGui();
|
|||
|
self.actionCall('skip');
|
|||
|
};
|
|||
|
|
|||
|
if (this.shouldDisplayEndTestWarning()) {
|
|||
|
this.displayEndTestWarning(doSkip);
|
|||
|
} else {
|
|||
|
doSkip();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Handles the timeout state
|
|||
|
*/
|
|||
|
timeout: function () {
|
|||
|
var self = this;
|
|||
|
this.disableGui();
|
|||
|
this.testContext.isTimeout = true;
|
|||
|
this.updateTimer();
|
|||
|
|
|||
|
this.killItemSession(function () {
|
|||
|
var confirmBox = $('.timeout-modal-feedback'),
|
|||
|
testContext = self.testContext,
|
|||
|
confirmBtn = confirmBox.find('.js-timeout-confirm, .modal-close');
|
|||
|
|
|||
|
if (testContext.numberCompletedSection === testContext.numberItemsSection) {
|
|||
|
testMetaData.addData({"SECTION" : {"SECTION_EXIT_CODE" : testMetaData.SECTION_EXIT_CODE.COMPLETE_TIMEOUT}});
|
|||
|
} else {
|
|||
|
testMetaData.addData({"SECTION" : {"SECTION_EXIT_CODE" : testMetaData.SECTION_EXIT_CODE.TIMEOUT}});
|
|||
|
}
|
|||
|
|
|||
|
self.enableGui();
|
|||
|
confirmBox.modal({width: 500});
|
|||
|
confirmBtn.off('click').on('click', function () {
|
|||
|
confirmBox.modal('close');
|
|||
|
self.actionCall('timeout');
|
|||
|
});
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Sets the assessment test context object
|
|||
|
* @param {Object} testContext
|
|||
|
*/
|
|||
|
setTestContext: function(testContext) {
|
|||
|
this.testContext = testContext;
|
|||
|
this.itemServiceApi = eval(testContext.itemServiceApiCall);
|
|||
|
this.itemServiceApi.setHasBeenPaused(testContext.hasBeenPaused);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Handles Metadata initialization
|
|||
|
*/
|
|||
|
initMetadata: function (){
|
|||
|
testMetaData = testMetaDataFactory({
|
|||
|
testServiceCallId: this.itemServiceApi.serviceCallId
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Retrieve service responsible for broken session tracking
|
|||
|
* @returns {*}
|
|||
|
*/
|
|||
|
getSessionStateService: function () {
|
|||
|
if (!sessionStateService) {
|
|||
|
sessionStateService = this.testContext.sessionStateService({accuracy: 1000});
|
|||
|
}
|
|||
|
return sessionStateService;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the GUI
|
|||
|
* @param {Object} testContext
|
|||
|
*/
|
|||
|
update: function (testContext) {
|
|||
|
var self = this;
|
|||
|
$controls.$itemFrame.remove();
|
|||
|
|
|||
|
var $runner = $('#runner');
|
|||
|
$runner.css('height', 'auto');
|
|||
|
|
|||
|
this.getSessionStateService().restart();
|
|||
|
|
|||
|
this.setTestContext(testContext);
|
|||
|
this.updateContext();
|
|||
|
this.updateProgress();
|
|||
|
this.updateNavigation();
|
|||
|
this.updateTestReview();
|
|||
|
this.updateInformation();
|
|||
|
this.updateRubrics();
|
|||
|
this.updateTools(testContext);
|
|||
|
this.updateTimer();
|
|||
|
this.updateExitButton();
|
|||
|
this.resetCurrentItemState();
|
|||
|
this.initMetadata();
|
|||
|
|
|||
|
$controls.$itemFrame = $('<iframe id="qti-item" frameborder="0" scrollbars="no"/>');
|
|||
|
$controls.$itemFrame.appendTo($controls.$contentBox);
|
|||
|
|
|||
|
if (this.testContext.itemSessionState === this.TEST_ITEM_STATE_INTERACTING && self.testContext.isTimeout === false) {
|
|||
|
$doc.off('.testRunner').on('serviceloaded.testRunner', function () {
|
|||
|
self.afterTransition();
|
|||
|
self.adjustFrame();
|
|||
|
$controls.$itemFrame.css({visibility: 'visible'});
|
|||
|
});
|
|||
|
|
|||
|
// Inject API into the frame.
|
|||
|
this.itemServiceApi.loadInto($controls.$itemFrame[0], function () {
|
|||
|
// We now rely on the 'serviceloaded' event.
|
|||
|
});
|
|||
|
}
|
|||
|
else {
|
|||
|
// e.g. no more attempts or timeout! Simply consider the transition is finished,
|
|||
|
// but do not load the item.
|
|||
|
self.afterTransition();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Displays feedback on the current state of the test
|
|||
|
*/
|
|||
|
updateInformation: function () {
|
|||
|
|
|||
|
if (this.testContext.isTimeout === true) {
|
|||
|
feedback().error(__('Time limit reached for item "%s".', this.testContext.itemIdentifier));
|
|||
|
}
|
|||
|
else if (this.testContext.itemSessionState !== this.TEST_ITEM_STATE_INTERACTING) {
|
|||
|
feedback().error(__('No more attempts allowed for item "%s".', this.testContext.itemIdentifier));
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the displayed tools
|
|||
|
* @param {Object} testContext
|
|||
|
*/
|
|||
|
updateTools: function updateTools(testContext) {
|
|||
|
var showSkip = false;
|
|||
|
var showSkipEnd = false;
|
|||
|
var showNextSection = !!testContext.nextSection && (this.hasOption(optionNextSection) || this.hasOption(optionNextSectionWarning));
|
|||
|
|
|||
|
if (this.testContext.allowSkipping === true) {
|
|||
|
if (this.testContext.isLast === false) {
|
|||
|
showSkip = true;
|
|||
|
} else {
|
|||
|
showSkipEnd = true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$controls.$skip.toggle(showSkip);
|
|||
|
$controls.$skipEnd.toggle(showSkipEnd);
|
|||
|
$controls.$nextSection.toggle(showNextSection);
|
|||
|
|
|||
|
actionBarTools.render('.tools-box-list', testContext, TestRunner);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Displays a timer
|
|||
|
* @param {Object} cst
|
|||
|
* @returns {*|jQuery|HTMLElement}
|
|||
|
*/
|
|||
|
createTimer: function(cst) {
|
|||
|
var $timer = $('<div>', {'class': 'qti-timer qti-timer__type-' + cst.qtiClassName }),
|
|||
|
$label = $('<div>', {'class': 'qti-timer_label truncate', text: cst.label }),
|
|||
|
$time = $('<div>', {'class': 'qti-timer_time', text: this.formatTime(cst.seconds) });
|
|||
|
|
|||
|
$timer.append($label);
|
|||
|
$timer.append($time);
|
|||
|
return $timer;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the timers
|
|||
|
*/
|
|||
|
updateTimer: function () {
|
|||
|
var self = this;
|
|||
|
var hasTimers;
|
|||
|
$controls.$timerWrapper.empty();
|
|||
|
|
|||
|
for (var i = 0; i < timerIds.length; i++) {
|
|||
|
clearTimeout(timerIds[i]);
|
|||
|
}
|
|||
|
|
|||
|
timerIds = [];
|
|||
|
currentTimes = [];
|
|||
|
lastDates = [];
|
|||
|
timeDiffs = [];
|
|||
|
|
|||
|
if (self.testContext.isTimeout === false &&
|
|||
|
self.testContext.itemSessionState === self.TEST_ITEM_STATE_INTERACTING) {
|
|||
|
|
|||
|
hasTimers = !!this.testContext.timeConstraints.length;
|
|||
|
$controls.$topActionBar.toggleClass('has-timers', hasTimers);
|
|||
|
|
|||
|
if (hasTimers) {
|
|||
|
|
|||
|
// Insert QTI Timers container.
|
|||
|
// self.formatTime(cst.seconds)
|
|||
|
for (i = 0; i < this.testContext.timeConstraints.length; i++) {
|
|||
|
|
|||
|
var cst = this.testContext.timeConstraints[i];
|
|||
|
|
|||
|
if (cst.allowLateSubmission === false) {
|
|||
|
|
|||
|
// Set up a timer for this constraint
|
|||
|
$controls.$timerWrapper.append(self.createTimer(cst));
|
|||
|
|
|||
|
// Set up a timer and update it with setInterval.
|
|||
|
currentTimes[i] = cst.seconds;
|
|||
|
lastDates[i] = new Date();
|
|||
|
timeDiffs[i] = 0;
|
|||
|
timerIndex = i;
|
|||
|
|
|||
|
if (self.testContext.timerWarning && self.testContext.timerWarning[cst.qtiClassName]) {
|
|||
|
cst.warnings = {};
|
|||
|
_(self.testContext.timerWarning[cst.qtiClassName]).forEach(function (value, key) {
|
|||
|
if (_.contains(['info', 'warning', 'danger'], value)) {
|
|||
|
cst.warnings[key] = {
|
|||
|
type: value,
|
|||
|
showed: cst.seconds <= key,
|
|||
|
point: parseInt(key, 10)
|
|||
|
};
|
|||
|
}
|
|||
|
});
|
|||
|
var closestPreviousWarning = _.find(cst.warnings, { showed: true });
|
|||
|
if (!_.isEmpty(closestPreviousWarning) && closestPreviousWarning.point) {
|
|||
|
cst.warnings[closestPreviousWarning.point].showed = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
(function (timerIndex, cst) {
|
|||
|
timerIds[timerIndex] = setInterval(function () {
|
|||
|
|
|||
|
timeDiffs[timerIndex] += (new Date()).getTime() - lastDates[timerIndex].getTime();
|
|||
|
|
|||
|
if (timeDiffs[timerIndex] >= 1000) {
|
|||
|
var seconds = timeDiffs[timerIndex] / 1000;
|
|||
|
currentTimes[timerIndex] -= seconds;
|
|||
|
timeDiffs[timerIndex] = 0;
|
|||
|
}
|
|||
|
|
|||
|
$timers.eq(timerIndex)
|
|||
|
.html(self.formatTime(Math.round(currentTimes[timerIndex])));
|
|||
|
|
|||
|
if (currentTimes[timerIndex] <= 0) {
|
|||
|
// The timer expired...
|
|||
|
currentTimes[timerIndex] = 0;
|
|||
|
clearInterval(timerIds[timerIndex]);
|
|||
|
|
|||
|
// Hide item to prevent any further interaction with the candidate.
|
|||
|
$controls.$itemFrame.hide();
|
|||
|
self.timeout();
|
|||
|
} else {
|
|||
|
lastDates[timerIndex] = new Date();
|
|||
|
}
|
|||
|
|
|||
|
var warning = _.findLast(cst.warnings, { showed: false });
|
|||
|
|
|||
|
if (!_.isEmpty(warning) && _.isFinite(warning.point) && currentTimes[timerIndex] <= warning.point) {
|
|||
|
self.timeWarning(cst, warning);
|
|||
|
}
|
|||
|
|
|||
|
}, 1000);
|
|||
|
}(timerIndex, cst));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$timers = $controls.$timerWrapper.find('.qti-timer .qti-timer_time');
|
|||
|
$controls.$timerWrapper.show();
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Mark appropriate timer by warning colors and show feedback message
|
|||
|
*
|
|||
|
* @param {object} cst - Time constraint
|
|||
|
* @param {integer} cst.warningTime - Warning time in seconds.
|
|||
|
* @param {integer} cst.qtiClassName - Class name of qti instance for which the timer is set (assessmentItemRef | assessmentSection | testPart | assessmentTest).
|
|||
|
* @param {integer} cst.seconds - Initial timer value.
|
|||
|
* @param {object} warning - Current actual warning
|
|||
|
* @param {integer} warning.point - Warning time point in seconds, when show message
|
|||
|
* @param {boolean} warning.showed - boolean flag for mark already showed warnings
|
|||
|
* @param {string} warning.type - type of warning (from config), can be info, warning or error
|
|||
|
*
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
timeWarning: function (cst, warning) {
|
|||
|
var message = '',
|
|||
|
remaining,
|
|||
|
$timer = $controls.$timerWrapper.find('.qti-timer__type-' + cst.qtiClassName),
|
|||
|
$time = $timer.find('.qti-timer_time');
|
|||
|
|
|||
|
$time.removeClass('txt-info txt-warning txt-danger').addClass('txt-' + warning.type);
|
|||
|
|
|||
|
remaining = moment.duration(warning.point, "seconds").humanize();
|
|||
|
|
|||
|
switch (cst.qtiClassName) {
|
|||
|
case 'assessmentItemRef':
|
|||
|
message = __("Warning – You have %s remaining to complete this item.", remaining);
|
|||
|
break;
|
|||
|
case 'assessmentSection':
|
|||
|
message = __("Warning – You have %s remaining to complete this section.", remaining);
|
|||
|
break;
|
|||
|
case 'testPart':
|
|||
|
message = __("Warning – You have %s remaining to complete this test part.", remaining);
|
|||
|
break;
|
|||
|
case 'assessmentTest':
|
|||
|
message = __("Warning – You have %s remaining to complete the test.", remaining);
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
feedback()[warning.type](message);
|
|||
|
|
|||
|
cst.warnings[warning.point].showed = true;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Displays or hides the rubric block
|
|||
|
*/
|
|||
|
updateRubrics: function () {
|
|||
|
$controls.$rubricBlocks.remove();
|
|||
|
|
|||
|
if (this.testContext.rubrics.length > 0) {
|
|||
|
|
|||
|
$controls.$rubricBlocks = $('<div id="qti-rubrics"/>');
|
|||
|
|
|||
|
for (var i = 0; i < this.testContext.rubrics.length; i++) {
|
|||
|
$controls.$rubricBlocks.append(this.testContext.rubrics[i]);
|
|||
|
}
|
|||
|
|
|||
|
// modify the <a> tags in order to be sure it
|
|||
|
// opens in another window.
|
|||
|
$controls.$rubricBlocks.find('a').bind('click keypress', function () {
|
|||
|
window.open(this.href);
|
|||
|
return false;
|
|||
|
});
|
|||
|
|
|||
|
$controls.$rubricBlocks.prependTo($controls.$contentBox);
|
|||
|
|
|||
|
if (MathJax) {
|
|||
|
MathJax.Hub.Queue(["Typeset", MathJax.Hub], $controls.$rubricBlocks[0]);
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the list of navigation buttons (previous, next, skip, etc.)
|
|||
|
*/
|
|||
|
updateNavigation: function () {
|
|||
|
$controls.$exit.show();
|
|||
|
|
|||
|
if(this.testContext.isLast === true) {
|
|||
|
$controls.$moveForward.hide();
|
|||
|
$controls.$moveEnd.show();
|
|||
|
}
|
|||
|
else {
|
|||
|
$controls.$moveForward.show();
|
|||
|
$controls.$moveEnd.hide();
|
|||
|
}
|
|||
|
if (this.testContext.navigationMode === this.TEST_NAVIGATION_LINEAR) {
|
|||
|
// LINEAR
|
|||
|
$controls.$moveBackward.hide();
|
|||
|
}
|
|||
|
else {
|
|||
|
// NONLINEAR
|
|||
|
$controls.$controls.show();
|
|||
|
if(this.testContext.canMoveBackward === true) {
|
|||
|
$controls.$moveBackward.show();
|
|||
|
}
|
|||
|
else {
|
|||
|
$controls.$moveBackward.hide();
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the test taker review screen
|
|||
|
*/
|
|||
|
updateTestReview: function() {
|
|||
|
var considerProgress = this.testContext.considerProgress === true;
|
|||
|
|
|||
|
if (this.testReview) {
|
|||
|
this.testReview.toggle(considerProgress && this.hasOption(optionReviewScreen));
|
|||
|
this.testReview.update(this.testContext);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the progress bar
|
|||
|
*/
|
|||
|
updateProgress: function () {
|
|||
|
var considerProgress = this.testContext.considerProgress === true;
|
|||
|
|
|||
|
$controls.$progressBox.css('visibility', considerProgress ? 'visible' : 'hidden');
|
|||
|
|
|||
|
if (considerProgress) {
|
|||
|
this.progressUpdater.update(this.testContext);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Updates the test informations
|
|||
|
*/
|
|||
|
updateContext: function () {
|
|||
|
|
|||
|
$controls.$title.text(this.testContext.testTitle);
|
|||
|
|
|||
|
// Visibility of section?
|
|||
|
var sectionText = (this.testContext.isDeepestSectionVisible === true) ? (' - ' + this.testContext.sectionTitle) : '';
|
|||
|
|
|||
|
$controls.$position.text(sectionText);
|
|||
|
$controls.$titleGroup.show();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Displays the right exit button
|
|||
|
*/
|
|||
|
updateExitButton : function(){
|
|||
|
|
|||
|
$controls.$logout.toggleClass('hidden', !this.testContext.logoutButton);
|
|||
|
$controls.$exit.toggleClass('hidden', !this.testContext.exitButton);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Ensures the frame has the right size
|
|||
|
*/
|
|||
|
adjustFrame: function () {
|
|||
|
var rubricHeight = $controls.$rubricBlocks.outerHeight(true) || 0;
|
|||
|
var frameContentHeight;
|
|||
|
var finalHeight = $(window).innerHeight() - $controls.$topActionBar.outerHeight() - $controls.$bottomActionBar.outerHeight();
|
|||
|
var itemFrame = $controls.$itemFrame.get(0);
|
|||
|
$controls.$contentBox.height(finalHeight);
|
|||
|
if($controls.$sideBars.length){
|
|||
|
$controls.$sideBars.each(function() {
|
|||
|
var $sideBar = $(this);
|
|||
|
$sideBar.height(finalHeight - $sideBar.outerHeight() + $sideBar.height());
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
if(itemFrame && itemFrame.contentWindow){
|
|||
|
frameContentHeight = $controls.$itemFrame.contents().outerHeight(true);
|
|||
|
|
|||
|
if (frameContentHeight < finalHeight) {
|
|||
|
if (rubricHeight) {
|
|||
|
frameContentHeight = Math.max(frameContentHeight, finalHeight - rubricHeight);
|
|||
|
} else {
|
|||
|
frameContentHeight = finalHeight;
|
|||
|
}
|
|||
|
}
|
|||
|
if (itemFrame.contentWindow.$) {
|
|||
|
itemFrame.contentWindow.$('body').trigger('setheight', [frameContentHeight]);
|
|||
|
}
|
|||
|
$controls.$itemFrame.height(frameContentHeight);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Locks the GUI
|
|||
|
*/
|
|||
|
disableGui: function () {
|
|||
|
$controls.$naviButtons.addClass('disabled');
|
|||
|
if (this.testReview) {
|
|||
|
this.testReview.disable();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Unlocks the GUI
|
|||
|
*/
|
|||
|
enableGui: function () {
|
|||
|
$controls.$naviButtons.removeClass('disabled');
|
|||
|
if (this.testReview) {
|
|||
|
this.testReview.enable();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Hides the GUI
|
|||
|
*/
|
|||
|
hideGui: function () {
|
|||
|
$controls.$naviButtons.addClass('hidden');
|
|||
|
if (this.testReview) {
|
|||
|
this.testReview.hide();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Shows the GUI
|
|||
|
*/
|
|||
|
showGui: function () {
|
|||
|
$controls.$naviButtons.removeClass('hidden');
|
|||
|
if (this.testReview) {
|
|||
|
this.testReview.show();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Formats a timer
|
|||
|
* @param {Number} totalSeconds
|
|||
|
* @returns {String}
|
|||
|
*/
|
|||
|
formatTime: function (totalSeconds) {
|
|||
|
var sec_num = totalSeconds;
|
|||
|
var hours = Math.floor(sec_num / 3600);
|
|||
|
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
|||
|
var seconds = Math.floor(sec_num - (hours * 3600) - (minutes * 60));
|
|||
|
|
|||
|
if (hours < 10) {
|
|||
|
hours = "0" + hours;
|
|||
|
}
|
|||
|
if (minutes < 10) {
|
|||
|
minutes = "0" + minutes;
|
|||
|
}
|
|||
|
if (seconds < 10) {
|
|||
|
seconds = "0" + seconds;
|
|||
|
}
|
|||
|
|
|||
|
var time = hours + ':' + minutes + ':' + seconds;
|
|||
|
|
|||
|
return time;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Processes an error
|
|||
|
* @param {Object} error
|
|||
|
*/
|
|||
|
processError : function processError(error) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
// keep disabled
|
|||
|
this.hideGui();
|
|||
|
this.beforeTransition();
|
|||
|
|
|||
|
// ask the parent to display a message
|
|||
|
iframeNotifier.parent('messagealert', {
|
|||
|
message : error.message,
|
|||
|
action : function() {
|
|||
|
if (testMetaData) {
|
|||
|
testMetaData.clearData();
|
|||
|
}
|
|||
|
if (error.state === self.TEST_STATE_CLOSED) {
|
|||
|
// test is closed, finish it
|
|||
|
self.serviceApi.finish();
|
|||
|
} else {
|
|||
|
// test is still open, just exit to the index
|
|||
|
self.serviceApi.exit();
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Call action specified in testContext. A postfix <i>Url</i> will be added to the action name.
|
|||
|
* To specify actions see {@link https://github.com/oat-sa/extension-tao-testqti/blob/master/helpers/class.TestRunnerUtils.php}
|
|||
|
* @param {String} action - Action name
|
|||
|
* @param {Object} [extraParams] - Additional parameters to be sent to the server
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
actionCall: function (action, extraParams) {
|
|||
|
var self = this,
|
|||
|
params = {metaData: testMetaData ? testMetaData.getData() : {}};
|
|||
|
|
|||
|
if (extraParams) {
|
|||
|
params = _.assign(params, extraParams);
|
|||
|
}
|
|||
|
this.beforeTransition(function () {
|
|||
|
$.ajax({
|
|||
|
url: self.testContext[action + 'Url'],
|
|||
|
cache: false,
|
|||
|
data: params,
|
|||
|
async: true,
|
|||
|
dataType: 'json',
|
|||
|
success: function (testContext) {
|
|||
|
testMetaData.clearData();
|
|||
|
|
|||
|
if (!testContext.success) {
|
|||
|
self.processError(testContext);
|
|||
|
}
|
|||
|
else if (testContext.state === self.TEST_STATE_CLOSED) {
|
|||
|
self.serviceApi.finish();
|
|||
|
}
|
|||
|
else {
|
|||
|
self.update(testContext);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Exit from test (after confirmation). All answered questions will be submitted.
|
|||
|
*
|
|||
|
* @returns {undefined}
|
|||
|
*/
|
|||
|
exit: function () {
|
|||
|
var self = this;
|
|||
|
testMetaData.addData({
|
|||
|
"TEST" : {"TEST_EXIT_CODE" : testMetaData.TEST_EXIT_CODE.INCOMPLETE},
|
|||
|
"SECTION" : {"SECTION_EXIT_CODE" : testMetaData.SECTION_EXIT_CODE.QUIT}
|
|||
|
});
|
|||
|
this.displayExitMessage(
|
|||
|
__('Are you sure you want to end the test?'),
|
|||
|
function() {
|
|||
|
self.killItemSession(function () {
|
|||
|
self.actionCall('endTestSession');
|
|||
|
testMetaData.clearData();
|
|||
|
});
|
|||
|
},
|
|||
|
{ scope: this.testReview ? this.testContext.reviewScope : null }
|
|||
|
);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Set the state of the current item in the test runner
|
|||
|
*
|
|||
|
* @param {string} id
|
|||
|
* @param {object} state
|
|||
|
*/
|
|||
|
setCurrentItemState : function(id, state){
|
|||
|
if(id){
|
|||
|
this.currentItemState[id] = state;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Reset the state of the current item in the test runner
|
|||
|
*/
|
|||
|
resetCurrentItemState : function(){
|
|||
|
this.currentItemState = {};
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Get the state of the current item as stored in the test runner
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
getCurrentItemState : function(){
|
|||
|
return this.currentItemState;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
var config = module.config();
|
|||
|
if (config) {
|
|||
|
actionBarTools.register(config.qtiTools);
|
|||
|
}
|
|||
|
|
|||
|
return {
|
|||
|
start: function (testContext) {
|
|||
|
|
|||
|
$controls = {
|
|||
|
// navigation
|
|||
|
$moveForward: $('[data-control="move-forward"]'),
|
|||
|
$moveEnd: $('[data-control="move-end"]'),
|
|||
|
$moveBackward: $('[data-control="move-backward"]'),
|
|||
|
$nextSection: $('[data-control="next-section"]'),
|
|||
|
$skip: $('[data-control="skip"]'),
|
|||
|
$skipEnd: $('[data-control="skip-end"]'),
|
|||
|
$exit: $(window.parent.document).find('[data-control="exit"]'),
|
|||
|
$logout: $(window.parent.document).find('[data-control="logout"]'),
|
|||
|
$naviButtons: $('.bottom-action-bar .action'),
|
|||
|
$skipButtons: $('.navi-box .skip'),
|
|||
|
$forwardButtons: $('.navi-box .forward'),
|
|||
|
|
|||
|
// progress bar
|
|||
|
$progressBar: $('[data-control="progress-bar"]'),
|
|||
|
$progressLabel: $('[data-control="progress-label"]'),
|
|||
|
$progressBox: $('.progress-box'),
|
|||
|
|
|||
|
// title
|
|||
|
$title: $('[data-control="qti-test-title"]'),
|
|||
|
$position: $('[data-control="qti-test-position"]'),
|
|||
|
|
|||
|
// timers
|
|||
|
$timerWrapper: $('[data-control="qti-timers"]'),
|
|||
|
|
|||
|
// other zones
|
|||
|
$contentPanel: $('.content-panel'),
|
|||
|
$controls: $('.qti-controls'),
|
|||
|
$itemFrame: $('#qti-item'),
|
|||
|
$rubricBlocks: $('#qti-rubrics'),
|
|||
|
$contentBox: $('#qti-content'),
|
|||
|
$sideBars: $('.test-sidebar'),
|
|||
|
$topActionBar: $('.horizontal-action-bar.top-action-bar'),
|
|||
|
$bottomActionBar: $('.horizontal-action-bar.bottom-action-bar')
|
|||
|
};
|
|||
|
|
|||
|
// title
|
|||
|
$controls.$titleGroup = $controls.$title.add($controls.$position);
|
|||
|
|
|||
|
$doc.ajaxError(function (event, jqxhr) {
|
|||
|
if (jqxhr.status === 403) {
|
|||
|
iframeNotifier.parent('serviceforbidden');
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
window.onServiceApiReady = function onServiceApiReady(serviceApi) {
|
|||
|
TestRunner.serviceApi = serviceApi;
|
|||
|
|
|||
|
if (!testContext.success) {
|
|||
|
TestRunner.processError(testContext);
|
|||
|
}
|
|||
|
|
|||
|
// If the assessment test session is in CLOSED state,
|
|||
|
// we give the control to the delivery engine by calling finish.
|
|||
|
else if (testContext.state === TestRunner.TEST_STATE_CLOSED) {
|
|||
|
serviceApi.finish();
|
|||
|
testMetaData.clearData();
|
|||
|
}
|
|||
|
else {
|
|||
|
|
|||
|
if (TestRunner.getSessionStateService().getDuration()) {
|
|||
|
TestRunner.setTestContext(testContext);
|
|||
|
TestRunner.initMetadata();
|
|||
|
|
|||
|
TestRunner.keepItemTimed(TestRunner.getSessionStateService().getDuration());
|
|||
|
TestRunner.getSessionStateService().restart();
|
|||
|
} else {
|
|||
|
TestRunner.update(testContext);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
TestRunner.beforeTransition();
|
|||
|
TestRunner.testContext = testContext;
|
|||
|
|
|||
|
$controls.$skipButtons.click(function () {
|
|||
|
if (!$(this).hasClass('disabled')) {
|
|||
|
TestRunner.skip();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
$controls.$forwardButtons.click(function () {
|
|||
|
if (!$(this).hasClass('disabled')) {
|
|||
|
TestRunner.moveForward();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
$controls.$moveBackward.click(function () {
|
|||
|
if (!$(this).hasClass('disabled')) {
|
|||
|
TestRunner.moveBackward();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
$controls.$nextSection.click(function () {
|
|||
|
if (!$(this).hasClass('disabled')) {
|
|||
|
TestRunner.nextSection();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
$controls.$exit.click(function (e) {
|
|||
|
e.preventDefault();
|
|||
|
TestRunner.exit();
|
|||
|
});
|
|||
|
|
|||
|
$(window).on('resize', _.throttle(function () {
|
|||
|
TestRunner.adjustFrame();
|
|||
|
$controls.$titleGroup.show();
|
|||
|
}, 250));
|
|||
|
|
|||
|
$doc.on('loading', function () {
|
|||
|
iframeNotifier.parent('loading');
|
|||
|
});
|
|||
|
|
|||
|
|
|||
|
$doc.on('unloading', function () {
|
|||
|
iframeNotifier.parent('unloading');
|
|||
|
});
|
|||
|
|
|||
|
TestRunner.progressUpdater = progressUpdater($controls.$progressBar, $controls.$progressLabel);
|
|||
|
|
|||
|
if (testContext.reviewScreen) {
|
|||
|
TestRunner.testReview = testReview($controls.$contentPanel, {
|
|||
|
region: testContext.reviewRegion || 'left',
|
|||
|
hidden: !TestRunner.hasOption(optionReviewScreen),
|
|||
|
reviewScope: testContext.reviewScope,
|
|||
|
preventsUnseen: !!testContext.reviewPreventsUnseen,
|
|||
|
canCollapse: !!testContext.reviewCanCollapse
|
|||
|
}).on('jump', function(event, position) {
|
|||
|
TestRunner.jump(position);
|
|||
|
}).on('mark', function(event, flag, position) {
|
|||
|
TestRunner.markForReview(flag, position);
|
|||
|
});
|
|||
|
$controls.$sideBars = $('.test-sidebar');
|
|||
|
}
|
|||
|
|
|||
|
TestRunner.updateProgress();
|
|||
|
TestRunner.updateTestReview();
|
|||
|
|
|||
|
iframeNotifier.parent('serviceready');
|
|||
|
|
|||
|
|
|||
|
TestRunner.adjustFrame();
|
|||
|
|
|||
|
$controls.$topActionBar.add($controls.$bottomActionBar).animate({ opacity: 1 }, 600);
|
|||
|
|
|||
|
deleter($('#feedback-box'));
|
|||
|
modal($('body'));
|
|||
|
|
|||
|
//listen to state change in the current item
|
|||
|
$(document).on('responsechange', function(e, responseId, response){
|
|||
|
if(responseId && response){
|
|||
|
TestRunner.setCurrentItemState(responseId, {response:response});
|
|||
|
}
|
|||
|
}).on('stateready', function(e, id, state){
|
|||
|
if(id && state){
|
|||
|
TestRunner.setCurrentItemState(id, state);
|
|||
|
}
|
|||
|
}).on('heightchange', function(e, height) {
|
|||
|
$controls.$itemFrame.height(height);
|
|||
|
});
|
|||
|
|
|||
|
}
|
|||
|
};
|
|||
|
});
|