Skip to content

Commit 71e88df

Browse files
authored
Fix: Added cmi.objectives _recordObjectives and id existence check (fixes #316) (#317)
1 parent df90b8d commit 71e88df

File tree

7 files changed

+60
-9
lines changed

7 files changed

+60
-9
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ Determines whether the history of the user's responses to questions should be pe
8080

8181
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.
8282

83+
##### \_shouldRecordObjectives (boolean)
84+
85+
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.
86+
8387
##### \_shouldCompress (boolean)
8488

8589
Allow variable LZMA compress on component state data. The default is `false`.

example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"_shouldStoreResponses": true,
77
"_shouldStoreAttempts": false,
88
"_shouldRecordInteractions": true,
9+
"_shouldRecordObjectives": true,
910
"_shouldCompress": false
1011
},
1112
"_reporting": {

js/adapt-offlineStorage-scorm.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,16 @@ export default class OfflineStorageScorm extends Backbone.Controller {
130130

131131
switch (name.toLowerCase()) {
132132
case 'interaction':
133+
if (!this.statefulSession.shouldRecordInteractions) return;
133134
return this.scorm.recordInteraction(...args);
134135
case 'objectivedescription':
136+
if (!this.statefulSession.shouldRecordObjectives) return;
135137
return this.scorm.recordObjectiveDescription(...args);
136138
case 'objectivestatus':
139+
if (!this.statefulSession.shouldRecordObjectives) return;
137140
return this.scorm.recordObjectiveStatus(...args);
138141
case 'objectivescore':
142+
if (!this.statefulSession.shouldRecordObjectives) return;
139143
return this.scorm.recordObjectiveScore(...args);
140144
case 'location':
141145
return this.scorm.setLessonLocation(...args);

js/adapt-stateful-session.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,19 @@ export default class StatefulSession extends Backbone.Controller {
1919
this._shouldStoreResponses = true;
2020
this._shouldStoreAttempts = false;
2121
this._shouldRecordInteractions = true;
22+
this._shouldRecordObjectives = true;
2223
this._uniqueInteractionIds = false;
2324
this.beginSession();
2425
}
2526

27+
get shouldRecordInteractions() {
28+
return this._shouldRecordInteractions;
29+
}
30+
31+
get shouldRecordObjectives() {
32+
return this._shouldRecordObjectives;
33+
}
34+
2635
beginSession() {
2736
this.listenTo(Adapt, {
2837
'app:dataReady': this.restoreSession,
@@ -45,6 +54,9 @@ export default class StatefulSession extends Backbone.Controller {
4554
if (tracking?._shouldRecordInteractions === false) {
4655
this._shouldRecordInteractions = false;
4756
}
57+
if (tracking?._shouldRecordObjectives === false) {
58+
this._shouldRecordObjectives = false;
59+
}
4860
const settings = config._advancedSettings;
4961
if (!settings) {
5062
// 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 {
166178
}
167179

168180
initializeContentObjectives() {
181+
if (!this.shouldRecordObjectives) return;
169182
Adapt.contentObjects.forEach(model => {
170183
if (model.isTypeGroup('course')) return;
171184
const id = model.get('_id');
@@ -195,6 +208,7 @@ export default class StatefulSession extends Backbone.Controller {
195208
}
196209

197210
onPageViewReady(view) {
211+
if (!this.shouldRecordObjectives) return;
198212
const model = view.model;
199213
if (model.get('_isComplete')) return;
200214
const id = model.get('_id');
@@ -203,7 +217,7 @@ export default class StatefulSession extends Backbone.Controller {
203217
}
204218

205219
onQuestionRecordInteraction(questionView) {
206-
if (!this._shouldRecordInteractions) return;
220+
if (!this.shouldRecordInteractions) return;
207221
if (!this.scorm.isSupported('cmi.interactions._count')) return;
208222
// View functions are deprecated: getResponseType, getResponse, isCorrect, getLatency
209223
const questionModel = questionView.model;
@@ -221,6 +235,7 @@ export default class StatefulSession extends Backbone.Controller {
221235
}
222236

223237
onContentObjectCompleteChange(model) {
238+
if (!this.shouldRecordObjectives) return;
224239
if (model.isTypeGroup('course')) return;
225240
const id = model.get('_id');
226241
const completionStatus = (model.get('_isComplete') ? COMPLETION_STATE.COMPLETED : COMPLETION_STATE.INCOMPLETE).asLowerCase;

js/scorm/wrapper.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class ScormWrapper {
8181
this.finishCalled = false;
8282
this.logger = Logger.getInstance();
8383
this.scorm = pipwerks.SCORM;
84-
this.maxCharLimitOverride = null
84+
this.maxCharLimitOverride = null;
8585
/**
8686
* Prevent the Pipwerks SCORM API wrapper's handling of the exit status
8787
*/
@@ -612,7 +612,7 @@ class ScormWrapper {
612612
const validate = (attribute, value) => {
613613
const isValid = value >= 0 && score <= 100;
614614
if (!isValid) this.logger.warn(`${attribute} must be between 0-100.`);
615-
}
615+
};
616616
validate(`${cmiPrefix}.score.raw`, score);
617617
validate(`${cmiPrefix}.score.min`, minScore);
618618
validate(`${cmiPrefix}.score.max`, maxScore);
@@ -687,6 +687,15 @@ class ScormWrapper {
687687
return count === '' ? 0 : count;
688688
}
689689

690+
hasObjectiveById(id) {
691+
const count = this.getObjectiveCount();
692+
for (let i = 0; i < count; i++) {
693+
const storedId = this.getValue(`cmi.objectives.${i}.id`);
694+
if (storedId === id) return true;
695+
}
696+
return false;
697+
}
698+
690699
getObjectiveIndexById(id) {
691700
const count = this.getObjectiveCount();
692701
for (let i = 0; i < count; i++) {
@@ -699,18 +708,20 @@ class ScormWrapper {
699708
recordObjectiveDescription(id, description) {
700709
if (!this.isSCORM2004() || !description) return;
701710
id = id.trim();
711+
const hasObjective = this.hasObjectiveById(id);
702712
const index = this.getObjectiveIndexById(id);
703713
const cmiPrefix = `cmi.objectives.${index}`;
704-
this.setValue(`${cmiPrefix}.id`, id);
714+
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
705715
this.setValue(`${cmiPrefix}.description`, description);
706716
}
707717

708718
recordObjectiveScore(id, score, minScore = 0, maxScore = 100, isPercentageBased = true) {
709719
if (!this.isChildSupported('cmi.objectives.n.id') || !this.isSupported('cmi.objectives._count')) return;
710720
id = id.trim();
721+
const hasObjective = this.hasObjectiveById(id);
711722
const index = this.getObjectiveIndexById(id);
712723
const cmiPrefix = `cmi.objectives.${index}`;
713-
this.setValue(`${cmiPrefix}.id`, id);
724+
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
714725
this.recordScore(cmiPrefix, score, minScore, maxScore, isPercentageBased);
715726
}
716727

@@ -725,9 +736,10 @@ class ScormWrapper {
725736
return;
726737
}
727738
id = id.trim();
739+
const hasObjective = this.hasObjectiveById(id);
728740
const index = this.getObjectiveIndexById(id);
729741
const cmiPrefix = `cmi.objectives.${index}`;
730-
this.setValue(`${cmiPrefix}.id`, id);
742+
if (!hasObjective) this.setValue(`${cmiPrefix}.id`, id);
731743
if (this.isSCORM2004()) {
732744
this.setValue(`${cmiPrefix}.completion_status`, completionStatus);
733745
this.setValue(`${cmiPrefix}.success_status`, successStatus);
@@ -741,7 +753,7 @@ class ScormWrapper {
741753
isValidCompletionStatus(status) {
742754
status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule
743755
if (this.isSCORM2004()) {
744-
switch(status) {
756+
switch (status) {
745757
case COMPLETION_STATE.UNKNOWN.asLowerCase:
746758
case COMPLETION_STATE.NOTATTEMPTED.asLowerCase:
747759
case COMPLETION_STATE.NOT_ATTEMPTED.asLowerCase: // mentioned in SCORM 2004 spec - mapped to 'not attempted'
@@ -750,7 +762,7 @@ class ScormWrapper {
750762
return true;
751763
}
752764
} else {
753-
switch(status) {
765+
switch (status) {
754766
case COMPLETION_STATE.NOTATTEMPTED.asLowerCase:
755767
case COMPLETION_STATE.BROWSED.asLowerCase:
756768
case COMPLETION_STATE.INCOMPLETE.asLowerCase:
@@ -766,7 +778,7 @@ class ScormWrapper {
766778
isValidSuccessStatus(status) {
767779
status = status.toLowerCase(); // workaround for some LMSs (e.g. Arena) not adhering to the all-lowercase rule
768780
if (this.isSCORM2004()) {
769-
switch(status) {
781+
switch (status) {
770782
case SUCCESS_STATE.UNKNOWN.asLowerCase:
771783
case SUCCESS_STATE.PASSED.asLowerCase:
772784
case SUCCESS_STATE.FAILED.asLowerCase:

properties.schema

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@
5757
"validators": [],
5858
"help": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields."
5959
},
60+
"_shouldRecordObjectives": {
61+
"type": "boolean",
62+
"required": false,
63+
"default": true,
64+
"title": "Record objectives",
65+
"inputType": "Checkbox",
66+
"validators": [],
67+
"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."
68+
},
6069
"_shouldCompress": {
6170
"type": "boolean",
6271
"required": false,

schema/config.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
"title": "Record interactions",
4242
"description": "If enabled, the course will record the user's responses to questions to the cmi.interactions SCORM data fields",
4343
"default": true
44+
},
45+
"_shouldRecordObjectives": {
46+
"type": "boolean",
47+
"title": "Record objectives",
48+
"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",
49+
"default": true
4450
}
4551
}
4652
},

0 commit comments

Comments
 (0)