diff --git a/README.md b/README.md index 8aac355f..3b0a9617 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,10 @@ Determines whether the history of the user's responses to questions should be pe Determines whether the user's responses to questions should be tracked to the `cmi.interactions` fields of the SCORM data model or not. Acceptable values are `true` or `false`. The default is `true`. Note that not all SCORM 1.2 conformant Learning Management Systems support `cmi.interactions`. The code will attempt to detect whether support is implemented or not and, if not, will fail gracefully. Occasionally the code is unable to detect when `cmi.interactions` are not supported, in those (rare) instances you can switch off interaction tracking using this property so as to avoid 'not supported' errors. You can also switch off interaction tracking for any individual question using the `_recordInteraction` property of question components. All core question components support recording of interactions, community components will not necessarily do so. +##### \_shouldRecordObjectives (boolean) + +Determines whether the user's content objects and their statuses should be tracked to the `cmi.objectives` fields of the SCORM data model or not. Acceptable values are `true` or `false`. The default is `true`. Note that not all SCORM 1.2 conformant Learning Management Systems support `cmi.objectives`. The code will attempt to detect whether support is implemented or not and, if not, will fail gracefully. Occasionally the code is unable to detect when `cmi.objectives` are not supported, in those (rare) instances you can switch off objectives using this property so as to avoid 'not supported' errors. + ##### \_shouldCompress (boolean) Allow variable LZMA compress on component state data. The default is `false`. diff --git a/example.json b/example.json index 84e347cb..419b4066 100644 --- a/example.json +++ b/example.json @@ -6,6 +6,7 @@ "_shouldStoreResponses": true, "_shouldStoreAttempts": false, "_shouldRecordInteractions": true, + "_shouldRecordObjectives": true, "_shouldCompress": false }, "_reporting": { diff --git a/js/adapt-offlineStorage-scorm.js b/js/adapt-offlineStorage-scorm.js index b09bd51a..1f65eba5 100644 --- a/js/adapt-offlineStorage-scorm.js +++ b/js/adapt-offlineStorage-scorm.js @@ -130,12 +130,16 @@ export default class OfflineStorageScorm extends Backbone.Controller { switch (name.toLowerCase()) { case 'interaction': + if (!this.statefulSession.shouldRecordInteractions) return; return this.scorm.recordInteraction(...args); case 'objectivedescription': + if (!this.statefulSession.shouldRecordObjectives) return; return this.scorm.recordObjectiveDescription(...args); case 'objectivestatus': + if (!this.statefulSession.shouldRecordObjectives) return; return this.scorm.recordObjectiveStatus(...args); case 'objectivescore': + if (!this.statefulSession.shouldRecordObjectives) return; return this.scorm.recordObjectiveScore(...args); case 'location': return this.scorm.setLessonLocation(...args); diff --git a/js/adapt-stateful-session.js b/js/adapt-stateful-session.js index b44462f7..4f5733dc 100644 --- a/js/adapt-stateful-session.js +++ b/js/adapt-stateful-session.js @@ -19,10 +19,19 @@ export default class StatefulSession extends Backbone.Controller { this._shouldStoreResponses = true; this._shouldStoreAttempts = false; this._shouldRecordInteractions = true; + this._shouldRecordObjectives = true; this._uniqueInteractionIds = false; this.beginSession(); } + get shouldRecordInteractions() { + return this._shouldRecordInteractions; + } + + get shouldRecordObjectives() { + return this._shouldRecordObjectives; + } + beginSession() { this.listenTo(Adapt, { 'app:dataReady': this.restoreSession, @@ -45,6 +54,9 @@ export default class StatefulSession extends Backbone.Controller { if (tracking?._shouldRecordInteractions === false) { this._shouldRecordInteractions = false; } + if (tracking?._shouldRecordObjectives === false) { + this._shouldRecordObjectives = false; + } const settings = config._advancedSettings; if (!settings) { // force use of SCORM 1.2 by default - some LMSes (SABA/Kallidus for instance) @@ -166,6 +178,7 @@ export default class StatefulSession extends Backbone.Controller { } initializeContentObjectives() { + if (!this.shouldRecordObjectives) return; Adapt.contentObjects.forEach(model => { if (model.isTypeGroup('course')) return; const id = model.get('_id'); @@ -195,6 +208,7 @@ export default class StatefulSession extends Backbone.Controller { } onPageViewReady(view) { + if (!this.shouldRecordObjectives) return; const model = view.model; if (model.get('_isComplete')) return; const id = model.get('_id'); @@ -203,7 +217,7 @@ export default class StatefulSession extends Backbone.Controller { } onQuestionRecordInteraction(questionView) { - if (!this._shouldRecordInteractions) return; + if (!this.shouldRecordInteractions) return; if (!this.scorm.isSupported('cmi.interactions._count')) return; // View functions are deprecated: getResponseType, getResponse, isCorrect, getLatency const questionModel = questionView.model; @@ -221,6 +235,7 @@ export default class StatefulSession extends Backbone.Controller { } onContentObjectCompleteChange(model) { + if (!this.shouldRecordObjectives) return; if (model.isTypeGroup('course')) return; const id = model.get('_id'); const completionStatus = (model.get('_isComplete') ? COMPLETION_STATE.COMPLETED : COMPLETION_STATE.INCOMPLETE).asLowerCase; diff --git a/js/scorm/wrapper.js b/js/scorm/wrapper.js index e03c7052..7a895525 100644 --- a/js/scorm/wrapper.js +++ b/js/scorm/wrapper.js @@ -81,7 +81,7 @@ class ScormWrapper { this.finishCalled = false; this.logger = Logger.getInstance(); this.scorm = pipwerks.SCORM; - this.maxCharLimitOverride = null + this.maxCharLimitOverride = null; /** * Prevent the Pipwerks SCORM API wrapper's handling of the exit status */ @@ -612,7 +612,7 @@ class ScormWrapper { const validate = (attribute, value) => { const isValid = value >= 0 && score <= 100; if (!isValid) this.logger.warn(`${attribute} must be between 0-100.`); - } + }; validate(`${cmiPrefix}.score.raw`, score); validate(`${cmiPrefix}.score.min`, minScore); validate(`${cmiPrefix}.score.max`, maxScore); @@ -687,6 +687,15 @@ class ScormWrapper { return count === '' ? 0 : count; } + hasObjectiveById(id) { + const count = this.getObjectiveCount(); + for (let i = 0; i < count; i++) { + const storedId = this.getValue(`cmi.objectives.${i}.id`); + if (storedId === id) return true; + } + return false; + } + getObjectiveIndexById(id) { const count = this.getObjectiveCount(); for (let i = 0; i < count; i++) { @@ -699,18 +708,20 @@ class ScormWrapper { recordObjectiveDescription(id, description) { if (!this.isSCORM2004() || !description) return; id = id.trim(); + const hasObjective = this.hasObjectiveById(id); const index = this.getObjectiveIndexById(id); const cmiPrefix = `cmi.objectives.${index}`; - this.setValue(`${cmiPrefix}.id`, id); + if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id); this.setValue(`${cmiPrefix}.description`, description); } recordObjectiveScore(id, score, minScore = 0, maxScore = 100, isPercentageBased = true) { if (!this.isChildSupported('cmi.objectives.n.id') || !this.isSupported('cmi.objectives._count')) return; id = id.trim(); + const hasObjective = this.hasObjectiveById(id); const index = this.getObjectiveIndexById(id); const cmiPrefix = `cmi.objectives.${index}`; - this.setValue(`${cmiPrefix}.id`, id); + if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id); this.recordScore(cmiPrefix, score, minScore, maxScore, isPercentageBased); } @@ -725,9 +736,10 @@ class ScormWrapper { return; } id = id.trim(); + const hasObjective = this.hasObjectiveById(id); const index = this.getObjectiveIndexById(id); const cmiPrefix = `cmi.objectives.${index}`; - this.setValue(`${cmiPrefix}.id`, id); + if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id); if (this.isSCORM2004()) { this.setValue(`${cmiPrefix}.completion_status`, completionStatus); this.setValue(`${cmiPrefix}.success_status`, successStatus); @@ -741,7 +753,7 @@ class ScormWrapper { isValidCompletionStatus(status) { status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule if (this.isSCORM2004()) { - switch(status) { + switch (status) { case COMPLETION_STATE.UNKNOWN.asLowerCase: case COMPLETION_STATE.NOTATTEMPTED.asLowerCase: case COMPLETION_STATE.NOT_ATTEMPTED.asLowerCase: // mentioned in SCORM 2004 spec - mapped to 'not attempted' @@ -750,7 +762,7 @@ class ScormWrapper { return true; } } else { - switch(status) { + switch (status) { case COMPLETION_STATE.NOTATTEMPTED.asLowerCase: case COMPLETION_STATE.BROWSED.asLowerCase: case COMPLETION_STATE.INCOMPLETE.asLowerCase: @@ -766,7 +778,7 @@ class ScormWrapper { isValidSuccessStatus(status) { status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule if (this.isSCORM2004()) { - switch(status) { + switch (status) { case SUCCESS_STATE.UNKNOWN.asLowerCase: case SUCCESS_STATE.PASSED.asLowerCase: case SUCCESS_STATE.FAILED.asLowerCase: diff --git a/properties.schema b/properties.schema index 7fd6eb6a..dc905e81 100644 --- a/properties.schema +++ b/properties.schema @@ -57,6 +57,15 @@ "validators": [], "help": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields." }, + "_shouldRecordObjectives": { + "type": "boolean", + "required": false, + "default": true, + "title": "Record objectives", + "inputType": "Checkbox", + "validators": [], + "help": "If enabled, the course will be able to record the status and scores of the course objectives to the cmi.objectives SCORM data fields." + }, "_shouldCompress": { "type": "boolean", "required": false, diff --git a/schema/config.schema.json b/schema/config.schema.json index ee81e043..f4abecb1 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -41,6 +41,12 @@ "title": "Record interactions", "description": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields", "default": true + }, + "_shouldRecordObjectives": { + "type": "boolean", + "title": "Record objectives", + "description": "If enabled, the course will be able to record the status and scores of the course objectives to the cmi.objectives SCORM data fields", + "default": true } } },