diff --git a/js/adapt-contrib-spoor.js b/js/adapt-contrib-spoor.js index 3de86f0d..afc554c1 100644 --- a/js/adapt-contrib-spoor.js +++ b/js/adapt-contrib-spoor.js @@ -5,11 +5,13 @@ */ define(function(require) { - var Adapt = require('coreJS/adapt'); - var scormAPI = require('extensions/adapt-contrib-spoor/js/SCORM_API_wrapper'); - var scormWrapper = require('extensions/adapt-contrib-spoor/js/scormWrapper').getInstance(); - var scormLog = require('extensions/adapt-contrib-spoor/js/logger'); - + var Adapt = require('coreJS/adapt'), + _ = require('underscore'), + scormAPI = require('extensions/adapt-contrib-spoor/js/SCORM_API_wrapper'), + scormWrapper = require('extensions/adapt-contrib-spoor/js/scormWrapper').getInstance(), + scormLog = require('extensions/adapt-contrib-spoor/js/logger'), + serialiser = require('./serialisers/default'); + var Spoor = Backbone.Model.extend({ defaults: { @@ -19,107 +21,38 @@ define(function(require) { }, initialize: function() { - //_.bindAll(this); - this.data = Adapt.config.get('_spoor'); - console.log(scormWrapper); - this.SCOStart() - $(window).unload(_.bind(this.SCOFinish, this)); - this.onDataReady(); + this.data = Adapt.config.get('_spoor'); + this.SCOStart() ; + $(window).unload(_.bind(this.SCOFinish, this)); + this.onDataReady(); }, - checkCompletionCriteria: function() { - var requirementArray = [this.data._tracking._requireCourseCompleted, this.data._tracking._requireAssessmentPassed]; - var actualArray = [Adapt.course.get('_isComplete'), Adapt.course.get('_isAssessmentPassed')]; - - var criteriaMet = true; - - _.each(requirementArray, function(requirement, index) { - if(requirement) { - if(!actualArray[index]) { - criteriaMet = false; - } - } - }, this); - - if(criteriaMet) { - this.setLessonStatus(this.data._reporting._onTrackingCriteriaMet); - } - }, + SCOStart: function() { + var sw = scormWrapper; + if (sw.initialize()) { + sw.setVersion("1.2"); + this.set('initialised', true); + var lessonStatus = sw.getStatus().toLowerCase(); - convertCompletionStringToArray: function(string) { - var completionArray = string.split(""); - for (var i = 0; i < completionArray.length; i++) { - if (completionArray[i] === "-") { - completionArray[i] = -1; - } else { - completionArray[i] = parseInt(completionArray[i], 10); + if (lessonStatus === "not attempted" || lessonStatus === "unknown" || lessonStatus === undefined) { + sw.setIncomplete(); } } - return completionArray; - }, - - convertCompletionArrayToString: function(array) { - var completionString = array.join(""); - return completionString.replace(/-1/g, "-"); - }, - - createCompletionString: function() { - if(Adapt.course.get('_isComplete')) { - return 'courseComplete'; - } - var _blockCompletionArray = this.get('_blockCompletionArray'); - - _.each(Adapt.blocks.models, function(blockModel) { - if (blockModel.get('_isComplete')) { - _blockCompletionArray[blockModel.get('_trackingId')] = 1; - } - }, this); - - this.set('_blockCompletionArray', _blockCompletionArray); - return this.convertCompletionArrayToString(_blockCompletionArray); }, - createCompletionObject: function() { - var completionObject = {spoor:{}}; - completionObject.spoor.completion = this.createCompletionString(); - completionObject.spoor._isAssessmentPassed = Adapt.course.get('_isAssessmentPassed') || false; - this.set('completionObject', completionObject); - return completionObject; - }, - - getDataIsAlreadyReported: function(completionObject) { - return JSON.stringify(this.get('_suspendData')) === JSON.stringify(completionObject); - }, - - markBlockAsComplete: function(parameters) { - var block = parameters.block; - - if (block.get('_isComplete')) { - return; - } - - block.getChildren().each(function(child) { - child.set('_isComplete', true); - }, this); - }, - - onCriterionMet: function(criterion) { - if (!Adapt.course.get(criterion)) { - Adapt.course.set(criterion, true); + SCOFinish:function() { + if (!this.get('_SCOFinishCalled')) { + this.set('SCOFinishCalled', true); + scormWrapper.finish(); } - this.sendCompletionString(); - this.checkCompletionCriteria(); }, onDataReady: function() { - this.setSessionData(); - this.setupBlockCompletionData(); - this.repopulateCompletionData(); + this.loadSuspendData(); + this.assignSessionId(); this.setupListeners(); - // may no longer be needed - DH -// Adapt.trigger("spoorReady"); }, - + setupListeners: function() { Adapt.blocks.on('change:_isComplete', this.onBlockComplete, this); Adapt.course.on('change:_isComplete', this.onCourseComplete, this); @@ -127,28 +60,50 @@ define(function(require) { Adapt.on('questionView:complete', this.onQuestionComplete, this); Adapt.on('questionView:reset', this.onQuestionReset, this); }, - - onCourseComplete: function() { - this.onCriterionMet("_isComplete"); + + loadSuspendData: function() { + var suspendData = scormWrapper.getSuspendData(); + + if (suspendData === "" || suspendData === " " || suspendData === undefined) { + this.set('_suspendData', serialiser.serialise()); + } else { + this.set('_suspendData', serialiser.deserialise(suspendData)); + } }, - + + assignSessionId: function () { + this.set({ + _sessionID: Math.random().toString(36).slice(-8) + }); + }, + onBlockComplete: function(block) { this.set('lastCompletedBlock', block); - this.sendCompletionString(); + this.persistSuspendData(); }, - + + onCourseComplete: function() { + if(Adapt.course.get('_isComplete') === true) { + this.set('_attempts', this.get('_attempts')+1); + } + _.defer(function waitForBlockComplete() { + this.persistSuspendData(); + }); + }, + onAssessmentComplete: function(event) { + if(this.data._tracking._shouldSubmitScore) { + scormWrapper.setScore(event.scoreAsPercent, 0, 100); + } if (event.isPass) { - this.onCriterionMet("_isAssessmentPassed"); + Adapt.course.set('_isAssessmentPassed', event.isPass); + this.persistSuspendData(); } else { var onAssessmentFailure = this.data._reporting._onAssessmentFailure; if (onAssessmentFailure !== "" && onAssessmentFailure !== "incomplete") { this.setLessonStatus(onAssessmentFailure); } } - if(this.data._tracking._shouldSubmitScore) { - scormWrapper.setScore(event.scoreAsPercent, 0, 100); - } }, onQuestionComplete: function(questionView) { @@ -156,69 +111,19 @@ define(function(require) { }, onQuestionReset: function(questionView) { - var sameSession = this.get('_sessionID') === questionView.model.get('_sessionID'); - if(!sameSession) { + if(this.get('_sessionID') !== questionView.model.get('_sessionID')) { questionView.model.set('_isEnabledOnRevisit', true); } }, - repopulateCompletionData: function() { - var suspendData = this.get('_suspendData'); - - if (suspendData.spoor.completion !== "") { - this.restoreProgress(suspendData); - } - }, - - restoreProgress: function(suspendData) { - if (suspendData.spoor.completion === "courseComplete") { - Adapt.course.set('_isComplete', true); - Adapt.course.setOnChildren('_isComplete', true); - } else { - _.each(this.get('_blockCompletionArray'), function(blockCompletion, blockTrackingId) { - if (blockCompletion === 1) { - this.markBlockAsComplete({block: Adapt.blocks.findWhere({_trackingId: blockTrackingId}), includeChildren: true}); - } - }, this); - } - Adapt.course.set('_isAssessmentPassed', suspendData.spoor._isAssessmentPassed); - this.set('_suspendData', suspendData); - this.sendCompletionString(); - }, - - SCOFinish:function() { - if (!this.get('_SCOFinishCalled')) { - this.set('SCOFinishCalled', true); - scormWrapper.finish(); - } - }, - - SCOStart: function() { - // this.set('scormWrapper', this.data._testingMode') ? this.get('testingLMS : ScormWrapper.getInstance()); - var sw = scormWrapper; - if (sw.initialize()) { - sw.setVersion("1.2"); - this.set('initialised', true); - var lessonStatus = sw.getStatus().toLowerCase(); - - if (lessonStatus == "not attempted" || lessonStatus == "unknown") { - sw.setIncomplete(); - } - } - }, + persistSuspendData: function(){ + var courseCriteriaMet = this.data._tracking._requireCourseCompleted ? Adapt.course.get('_isComplete') : true, + assessmentCriteriaMet = this.data._tracking._requireAssessmentPassed ? Adapt.course.get('_isAssessmentPassed') : true; - sendCompletionString: function(){ - var suspendData = this.createCompletionObject(); - if (!this.getDataIsAlreadyReported(suspendData)) { - if (this.get('_lastCompletedBlock') !== undefined) { - console.info("Spoor: block " + this.get('_lastCompletedBlock').get('id') + " is complete."); - } - this.set({ - _suspendData: suspendData, - _blockCompletionArray: this.convertCompletionStringToArray(suspendData.spoor.completion) - }); - scormWrapper.setSuspendData(JSON.stringify(suspendData)); + if(courseCriteriaMet && assessmentCriteriaMet) { + this.setLessonStatus(this.data._reporting._onTrackingCriteriaMet); } + scormWrapper.setSuspendData(JSON.stringify(serialiser.serialise())); }, setLessonStatus:function(status){ @@ -239,74 +144,10 @@ define(function(require) { console.warn("cmi.core.lesson_status of " + status + " is not supported."); break; } - }, - - setScore: function(score){ - scormWrapper.setScore(score,0,100); - }, - - setSessionData: function() { - var suspendData = scormWrapper.getSuspendData(); - - if (suspendData === "" || suspendData === " " || suspendData === undefined) { - this.set('_suspendData', { - spoor: { - completion: "", - _isAssessmentPassed: false - } - }); - } else { - this.set('_suspendData', JSON.parse(suspendData)); - } - - this.set({ - _sessionID: this.generateRandomID(8) - }); - }, - - setTestingMode: function(value) { - this.set('testingLMS', value ? new TestingLMS() : null); - this.set('testingMode', value); - }, - - setupBlockCompletionData: function() { - if (Adapt.course.get('_latestTrackingId') === undefined) { - var message = "This course is missing a latestTrackingID.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; - alert(message); - console.error(message); - } - var _blockCompletionArray = new Array(Adapt.course.get('_latestTrackingId') + 1); - for (var i = 0; i < _blockCompletionArray.length; i++) { - _blockCompletionArray[i] = -1; - } - - _.each(Adapt.blocks.models, function(model, index) { - var _trackingId = model.get('_trackingId'); - var recordedCompletion = parseInt(this.get('_suspendData').spoor.completion[_trackingId], 10) || 0; - if (_trackingId === undefined) { - var message = "Block '" + model.get('id') + "' doesn't have a tracking ID assigned.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; - alert(message); - console.error(message); - } - _blockCompletionArray[_trackingId] = recordedCompletion; - }, this); - this.set('_blockCompletionArray', _blockCompletionArray); - }, - - parseBool: function(str) { - if(_.isString(str)) { - if (typeof str === 'string' && str.toLowerCase() == 'true') - return true; - return (parseInt(str) > 0); - } else if (_.isBoolean(str)) return str; - }, - - generateRandomID: function(numberOfCharacters) { - return Math.random().toString(36).slice(-numberOfCharacters); } - + }); Adapt.on('app:dataReady', function() { new Spoor(); - }) + }); }); \ No newline at end of file diff --git a/js/serialisers/default.js b/js/serialisers/default.js new file mode 100644 index 00000000..94e82fba --- /dev/null +++ b/js/serialisers/default.js @@ -0,0 +1,86 @@ +define(['coreJS/adapt'], function (Adapt) { + return { + + serialise: function () { + return { + spoor: { + completion: this.serialiseSaveState('_isComplete'), + _isCourseComplete: Adapt.course.get('_isComplete') || false, + _isAssessmentPassed: Adapt.course.get('_isAssessmentPassed') || false + } + }; + }, + + serialiseSaveState: function(attribute) { + if (Adapt.course.get('_latestTrackingId') === undefined) { + var message = "This course is missing a latestTrackingID.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; + console.error(message); + } + var excludeAssessments = Adapt.config.get('_spoor') && Adapt.config.get('_spoor')._tracking && Adapt.config.get('_spoor')._tracking._excludeAssessments, + data = new Array(Adapt.course.get('_latestTrackingId') + 1); + + for (var i = 0; i < data.length; i++) { + data[i] = -1; + } + + _.each(Adapt.blocks.models, function(model, index) { + var _trackingId = model.get('_trackingId'), + isPartOfAssessment = model.getParent().get('_assessment'), + state = model.get(attribute) ? 1: 0; + + if(excludeAssessments && isPartOfAssessment) { + state = 0; + } + + if (_trackingId === undefined) { + var message = "Block '" + model.get('id') + "' doesn't have a tracking ID assigned.\n\nPlease run the grunt process prior to deploying this module on LMS.\n\nScorm tracking will not work correctly until this is done."; + console.error(message); + } + + data[_trackingId] = state; + }, this); + + return data.join("").replace(/-1/g, "-"); + }, + + deserialise: function (data) { + var suspendData = JSON.parse(data); + + _.each(this.deserialiseSaveState(suspendData.spoor.completion), function(state, blockTrackingId) { + if (state === 1) { + this.markBlockAsComplete(Adapt.blocks.findWhere({_trackingId: blockTrackingId})); + } + }, this); + + Adapt.course.set('_isComplete', suspendData.spoor._isCourseComplete); + Adapt.course.set('_isAssessmentPassed', suspendData.spoor._isAssessmentPassed); + + return suspendData; + }, + + markBlockAsComplete: function(block) { + if (!block || block.get('_isComplete')) { + return; + } + + block.getChildren().each(function(child) { + child.set('_isComplete', true); + }, this); + }, + + deserialiseSaveState: function (string) { + var completionArray = string.split(""); + + for (var i = 0; i < completionArray.length; i++) { + if (completionArray[i] === "-") { + completionArray[i] = -1; + } else { + completionArray[i] = parseInt(completionArray[i], 10); + } + } + + return completionArray; + } + + }; +}); \ No newline at end of file