Skip to content

Commit

Permalink
New: Added cmi.objectives support (fixes #279). (#280)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielghost authored Sep 21, 2023
1 parent 0d3a937 commit cf911c5
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 76 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# adapt-contrib-spoor

**Spoor** is an *extension* bundled with the [Adapt framework](https://github.com/adaptlearning/adapt_framework).
**Spoor** is an *extension* bundled with the [Adapt framework](https://github.com/adaptlearning/adapt_framework).

This extension provides course tracking functionality (hence the name [spoor](https://en.wikipedia.org/wiki/Spoor_(animal))) via [SCORM](https://en.wikipedia.org/wiki/Sharable_Content_Object_Reference_Model) standards for compliant [Learning Management Systems (LMS)](https://en.wikipedia.org/wiki/Learning_management_system). As default, only SCORM 1.2 or SCORM 2004 4th Edition files are included. See [_scormVersion](https://github.com/adaptlearning/adapt-contrib-spoor#_scormVersion) for details on how to configure this accordingly.

Expand Down Expand Up @@ -202,4 +202,4 @@ Currently (officially) only supports SCORM 1.2
**Author / maintainer:** Adapt Core Team with [contributors](https://github.com/adaptlearning/adapt-contrib-spoor/graphs/contributors)
**Accessibility support:** n/a
**RTL support:** n/a
**Cross-platform coverage:** Chrome, Chrome for Android, Firefox (ESR + latest version), Edge, IE11, Safari 14 for macOS/iOS/iPadOS, Opera
**Cross-platform coverage:** Chrome, Chrome for Android, Firefox (ESR + latest version), Edge, Safari 14 for macOS/iOS/iPadOS, Opera
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "adapt-contrib-spoor",
"version": "5.8.2",
"framework": ">=5.28.1",
"framework": ">=5.31.31",
"homepage": "https://github.com/adaptlearning/adapt-contrib-spoor",
"bugs": "https://github.com/adaptlearning/adapt-contrib-spoor/issues",
"extension": "spoor",
Expand Down
6 changes: 6 additions & 0 deletions js/adapt-offlineStorage-scorm.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ export default class OfflineStorageScorm extends Backbone.Controller {
switch (name.toLowerCase()) {
case 'interaction':
return this.scorm.recordInteraction(...args);
case 'objectivedescription':
return this.scorm.recordObjectiveDescription(...args);
case 'objectivestatus':
return this.scorm.recordObjectiveStatus(...args);
case 'objectivescore':
return this.scorm.recordObjectiveScore(...args);
case 'location':
return this.scorm.setLessonLocation(...args);
case 'score':
Expand Down
46 changes: 42 additions & 4 deletions js/adapt-stateful-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export default class StatefulSession extends Backbone.Controller {
}

beginSession() {
this.listenTo(Adapt, 'app:dataReady', this.restoreSession);
this.listenTo(Adapt, {
'app:dataReady': this.restoreSession,
'adapt:start': this.onAdaptStart
});
this._trackingIdType = Adapt.build.get('trackingIdType') || 'block';
// suppress SCORM errors if 'nolmserrors' is found in the querystring
if (window.location.search.indexOf('nolmserrors') !== -1) {
Expand Down Expand Up @@ -59,8 +62,6 @@ export default class StatefulSession extends Backbone.Controller {
restoreSession() {
this.setupLearnerInfo();
this.restoreSessionState();
// defer call because AdaptModel.check*Status functions are asynchronous
_.defer(this.setupEventListeners.bind(this));
}

setupLearnerInfo() {
Expand Down Expand Up @@ -93,13 +94,16 @@ export default class StatefulSession extends Backbone.Controller {
setupEventListeners() {
this.removeEventListeners();
this.listenTo(Adapt.components, 'change:_isComplete', this.debouncedSaveSession);
this.listenTo(Adapt.contentObjects, 'change:_isComplete', this.onContentObjectCompleteChange);
this.listenTo(Adapt.course, 'change:_isComplete', this.debouncedSaveSession);
if (this._shouldStoreResponses) {
this.listenTo(data, 'change:_isSubmitted change:_userAnswer', this.debouncedSaveSession);
}
this.listenTo(Adapt, {
'app:dataReady': this.restoreSession,
'adapt:start': this.onAdaptStart,
'app:languageChanged': this.onLanguageChanged,
'pageView:ready': this.onPageViewReady,
'questionView:recordInteraction': this.onQuestionRecordInteraction,
'tracking:complete': this.onTrackingComplete
});
Expand Down Expand Up @@ -161,16 +165,43 @@ export default class StatefulSession extends Backbone.Controller {
logging.info(`course._isComplete: ${courseComplete}, course._isAssessmentPassed: ${assessmentPassed}, ${this._trackingIdType} completion: ${completionString}`);
}

initializeContentObjectives() {
Adapt.contentObjects.forEach(model => {
if (model.isTypeGroup('course')) return;
const id = model.get('_id');
const description = model.get('title') || model.get('displayTitle');
offlineStorage.set('objectiveDescription', id, description);
if (model.get('_isVisited')) return;
const completionStatus = COMPLETION_STATE.NOTATTEMPTED.asLowerCase;
offlineStorage.set('objectiveStatus', id, completionStatus);
});
}

onAdaptStart() {
this.setupEventListeners();
this.initializeContentObjectives();
}

onLanguageChanged() {
this.stopListening(Adapt.contentObjects, 'change:_isComplete', this.onContentObjectCompleteChange);
const config = Adapt.spoor.config;
if (config?._reporting?._resetStatusOnLanguageChange !== true) return;
offlineStorage.set('status', 'incomplete');
const completionStatus = COMPLETION_STATE.INCOMPLETE.asLowerCase;
offlineStorage.set('status', completionStatus);
}

onVisibilityChange() {
if (document.visibilityState === 'hidden') this.scorm.commit();
}

onPageViewReady(view) {
const model = view.model;
if (model.get('_isComplete')) return;
const id = model.get('_id');
const completionStatus = COMPLETION_STATE.INCOMPLETE.asLowerCase;
offlineStorage.set('objectiveStatus', id, completionStatus);
}

onQuestionRecordInteraction(questionView) {
if (!this._shouldRecordInteractions) return;
// View functions are deprecated: getResponseType, getResponse, isCorrect, getLatency
Expand All @@ -188,6 +219,13 @@ export default class StatefulSession extends Backbone.Controller {
offlineStorage.set('interaction', id, response, result, latency, responseType);
}

onContentObjectCompleteChange(model) {
if (model.isTypeGroup('course')) return;
const id = model.get('_id');
const completionStatus = (model.get('_isComplete') ? COMPLETION_STATE.COMPLETED : COMPLETION_STATE.INCOMPLETE).asLowerCase;
offlineStorage.set('objectiveStatus', id, completionStatus);
}

onTrackingComplete(completionData) {
const config = Adapt.spoor.config;
this.saveSessionState();
Expand Down
10 changes: 10 additions & 0 deletions js/enums/completionStateEnum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const COMPLETION_STATE = ENUM([
'UNKNOWN',
['NOT ATTEMPTED', 'NOTATTEMPTED'],
'NOT_ATTEMPTED',
'BROWSED',
'INCOMPLETE',
'COMPLETED'
]);

export default COMPLETION_STATE;
7 changes: 7 additions & 0 deletions js/enums/successStateEnum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const SUCCESS_STATUS = ENUM([
'UNKNOWN',
'PASSED',
'FAILED'
]);

export default SUCCESS_STATUS;
2 changes: 2 additions & 0 deletions js/scorm/cookieLMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export function start () {
configure();
this.initialize({
'cmi.interactions': [],
'cmi.objectives': [],
'cmi.core.lesson_status': 'not attempted',
'cmi.suspend_data': '',
'cmi.core.student_name': 'Surname, Sam',
Expand Down Expand Up @@ -199,6 +200,7 @@ export function start () {
configure();
this.initialize({
'cmi.interactions': [],
'cmi.objectives': [],
'cmi.completion_status': 'not attempted',
'cmi.suspend_data': '',
'cmi.learner_name': 'Surname, Sam',
Expand Down
Loading

0 comments on commit cf911c5

Please sign in to comment.