Working on the saving of data
This commit is contained in:
11
src/AICC.js
11
src/AICC.js
@@ -13,9 +13,16 @@ import {
|
||||
export default class AICC extends Scorm12API {
|
||||
/**
|
||||
* Constructor to create AICC API object
|
||||
* @param {object} settings
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
constructor(settings: {}) {
|
||||
const finalSettings = {
|
||||
...{
|
||||
mastery_override: false,
|
||||
}, ...settings,
|
||||
};
|
||||
|
||||
super(finalSettings);
|
||||
|
||||
this.cmi = new CMI();
|
||||
this.nav = new NAV();
|
||||
|
||||
193
src/BaseAPI.js
193
src/BaseAPI.js
@@ -1,19 +1,8 @@
|
||||
// @flow
|
||||
import {CMIArray} from './cmi/common';
|
||||
import {ValidationError} from './exceptions';
|
||||
|
||||
const api_constants = {
|
||||
SCORM_TRUE: 'true',
|
||||
SCORM_FALSE: 'false',
|
||||
STATE_NOT_INITIALIZED: 0,
|
||||
STATE_INITIALIZED: 1,
|
||||
STATE_TERMINATED: 2,
|
||||
LOG_LEVEL_DEBUG: 1,
|
||||
LOG_LEVEL_INFO: 2,
|
||||
LOG_LEVEL_WARNING: 3,
|
||||
LOG_LEVEL_ERROR: 4,
|
||||
LOG_LEVEL_NONE: 5,
|
||||
};
|
||||
import {scorm12_error_codes} from './constants/error_codes';
|
||||
import {global_constants} from './constants/api_constants';
|
||||
|
||||
/**
|
||||
* Base API class for AICC, SCORM 1.2, and SCORM 2004. Should be considered
|
||||
@@ -22,24 +11,34 @@ const api_constants = {
|
||||
export default class BaseAPI {
|
||||
#timeout;
|
||||
#error_codes;
|
||||
#settings = {
|
||||
autocommit: false,
|
||||
autocommitSeconds: 60,
|
||||
lmsCommitUrl: false,
|
||||
dataCommitFormat: 'json', // valid formats are 'json' or 'flattened', 'params'
|
||||
};
|
||||
cmi;
|
||||
startingData: {};
|
||||
|
||||
/**
|
||||
* Constructor for Base API class. Sets some shared API fields, as well as
|
||||
* sets up options for the API.
|
||||
* @param {object} error_codes
|
||||
* @param {object} settings
|
||||
*/
|
||||
constructor(error_codes) {
|
||||
constructor(error_codes, settings) {
|
||||
if (new.target === BaseAPI) {
|
||||
throw new TypeError('Cannot construct BaseAPI instances directly');
|
||||
}
|
||||
this.currentState = api_constants.STATE_NOT_INITIALIZED;
|
||||
this.apiLogLevel = api_constants.LOG_LEVEL_ERROR;
|
||||
this.currentState = global_constants.STATE_NOT_INITIALIZED;
|
||||
this.apiLogLevel = global_constants.LOG_LEVEL_ERROR;
|
||||
this.lastErrorCode = 0;
|
||||
this.listenerArray = [];
|
||||
|
||||
this.#timeout = null;
|
||||
this.#error_codes = error_codes;
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,26 +52,42 @@ export default class BaseAPI {
|
||||
callbackName: String,
|
||||
initializeMessage?: String,
|
||||
terminationMessage?: String) {
|
||||
let returnValue = api_constants.SCORM_FALSE;
|
||||
let returnValue = global_constants.SCORM_FALSE;
|
||||
|
||||
if (this.isInitialized()) {
|
||||
this.throwSCORMError(this.#error_codes.INITIALIZED, initializeMessage);
|
||||
} else if (this.isTerminated()) {
|
||||
this.throwSCORMError(this.#error_codes.TERMINATED, terminationMessage);
|
||||
} else {
|
||||
this.currentState = api_constants.STATE_INITIALIZED;
|
||||
this.currentState = global_constants.STATE_INITIALIZED;
|
||||
this.lastErrorCode = 0;
|
||||
returnValue = api_constants.SCORM_TRUE;
|
||||
returnValue = global_constants.SCORM_TRUE;
|
||||
this.processListeners(callbackName);
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
this.clearSCORMError(returnValue);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for #settings
|
||||
* @return {object}
|
||||
*/
|
||||
get settings() {
|
||||
return this.#settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for #settings
|
||||
* @param {object} settings
|
||||
*/
|
||||
set settings(settings: Object) {
|
||||
this.#settings = {...this.#settings, ...settings};
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the current run of the API
|
||||
* @param {string} callbackName
|
||||
@@ -82,19 +97,19 @@ export default class BaseAPI {
|
||||
terminate(
|
||||
callbackName: String,
|
||||
checkTerminated: boolean) {
|
||||
let returnValue = api_constants.SCORM_FALSE;
|
||||
let returnValue = global_constants.SCORM_FALSE;
|
||||
|
||||
if (this.checkState(checkTerminated,
|
||||
this.#error_codes.TERMINATION_BEFORE_INIT,
|
||||
this.#error_codes.MULTIPLE_TERMINATION)) {
|
||||
if (checkTerminated) this.lastErrorCode = 0;
|
||||
this.currentState = api_constants.STATE_TERMINATED;
|
||||
returnValue = api_constants.SCORM_TRUE;
|
||||
this.currentState = global_constants.STATE_TERMINATED;
|
||||
returnValue = global_constants.SCORM_TRUE;
|
||||
this.processListeners(callbackName);
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
this.clearSCORMError(returnValue);
|
||||
|
||||
return returnValue;
|
||||
@@ -123,7 +138,7 @@ export default class BaseAPI {
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, CMIElement, ': returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
this.clearSCORMError(returnValue);
|
||||
|
||||
return returnValue;
|
||||
@@ -143,7 +158,7 @@ export default class BaseAPI {
|
||||
checkTerminated: boolean,
|
||||
CMIElement,
|
||||
value) {
|
||||
let returnValue = api_constants.SCORM_FALSE;
|
||||
let returnValue = global_constants.SCORM_FALSE;
|
||||
|
||||
if (this.checkState(checkTerminated, this.#error_codes.STORE_BEFORE_INIT,
|
||||
this.#error_codes.STORE_AFTER_TERM)) {
|
||||
@@ -153,7 +168,7 @@ export default class BaseAPI {
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
this.lastErrorCode = e.errorCode;
|
||||
returnValue = api_constants.SCORM_FALSE;
|
||||
returnValue = global_constants.SCORM_FALSE;
|
||||
} else {
|
||||
this.throwSCORMError(this.#error_codes.GENERAL);
|
||||
}
|
||||
@@ -162,12 +177,20 @@ export default class BaseAPI {
|
||||
}
|
||||
|
||||
if (returnValue === undefined) {
|
||||
returnValue = api_constants.SCORM_FALSE;
|
||||
returnValue = global_constants.SCORM_FALSE;
|
||||
}
|
||||
|
||||
// If we didn't have any errors while setting the data, go ahead and
|
||||
// schedule a commit, if autocommit is turned on
|
||||
if (String(this.lastErrorCode) === '0') {
|
||||
if (this.#settings.autocommit && this.#timeout === undefined) {
|
||||
this.scheduleCommit(this.#settings.autocommitSeconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, CMIElement,
|
||||
': ' + value + ': result: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
this.clearSCORMError(returnValue);
|
||||
|
||||
return returnValue;
|
||||
@@ -182,17 +205,29 @@ export default class BaseAPI {
|
||||
commit(
|
||||
callbackName: String,
|
||||
checkTerminated: boolean) {
|
||||
let returnValue = api_constants.SCORM_FALSE;
|
||||
this.clearScheduledCommit();
|
||||
|
||||
let returnValue = global_constants.SCORM_FALSE;
|
||||
|
||||
if (this.checkState(checkTerminated, this.#error_codes.COMMIT_BEFORE_INIT,
|
||||
this.#error_codes.COMMIT_AFTER_TERM)) {
|
||||
const result = this.storeData(false);
|
||||
if (result.errorCode && result.errorCode > 0) {
|
||||
this.throwSCORMError(result.errorCode);
|
||||
}
|
||||
returnValue = result.result ?
|
||||
result.result : global_constants.SCORM_FALSE;
|
||||
|
||||
this.apiLog(callbackName, 'HttpRequest', ' Result: ' + returnValue,
|
||||
global_constants.LOG_LEVEL_DEBUG);
|
||||
|
||||
if (checkTerminated) this.lastErrorCode = 0;
|
||||
returnValue = api_constants.SCORM_TRUE;
|
||||
|
||||
this.processListeners(callbackName);
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
this.clearSCORMError(returnValue);
|
||||
|
||||
return returnValue;
|
||||
@@ -209,7 +244,7 @@ export default class BaseAPI {
|
||||
this.processListeners(callbackName);
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
@@ -230,7 +265,7 @@ export default class BaseAPI {
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
@@ -251,7 +286,7 @@ export default class BaseAPI {
|
||||
}
|
||||
|
||||
this.apiLog(callbackName, null, 'returned: ' + returnValue,
|
||||
api_constants.LOG_LEVEL_INFO);
|
||||
global_constants.LOG_LEVEL_INFO);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
@@ -296,13 +331,13 @@ export default class BaseAPI {
|
||||
|
||||
if (messageLevel >= this.apiLogLevel) {
|
||||
switch (messageLevel) {
|
||||
case api_constants.LOG_LEVEL_ERROR:
|
||||
case global_constants.LOG_LEVEL_ERROR:
|
||||
console.error(logMessage);
|
||||
break;
|
||||
case api_constants.LOG_LEVEL_WARNING:
|
||||
case global_constants.LOG_LEVEL_WARNING:
|
||||
console.warn(logMessage);
|
||||
break;
|
||||
case api_constants.LOG_LEVEL_INFO:
|
||||
case global_constants.LOG_LEVEL_INFO:
|
||||
console.info(logMessage);
|
||||
break;
|
||||
}
|
||||
@@ -426,12 +461,12 @@ export default class BaseAPI {
|
||||
_commonSetCMIValue(
|
||||
methodName: String, scorm2004: boolean, CMIElement, value) {
|
||||
if (!CMIElement || CMIElement === '') {
|
||||
return api_constants.SCORM_FALSE;
|
||||
return global_constants.SCORM_FALSE;
|
||||
}
|
||||
|
||||
const structure = CMIElement.split('.');
|
||||
let refObject = this;
|
||||
let returnValue = api_constants.SCORM_FALSE;
|
||||
let returnValue = global_constants.SCORM_FALSE;
|
||||
let foundFirstIndex = false;
|
||||
|
||||
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
||||
@@ -455,7 +490,7 @@ export default class BaseAPI {
|
||||
|
||||
if (!scorm2004 || this.lastErrorCode === 0) {
|
||||
refObject[attribute] = value;
|
||||
returnValue = api_constants.SCORM_TRUE;
|
||||
returnValue = global_constants.SCORM_TRUE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -496,10 +531,10 @@ export default class BaseAPI {
|
||||
}
|
||||
}
|
||||
|
||||
if (returnValue === api_constants.SCORM_FALSE) {
|
||||
if (returnValue === global_constants.SCORM_FALSE) {
|
||||
this.apiLog(methodName, null,
|
||||
`There was an error setting the value for: ${CMIElement}, value of: ${value}`,
|
||||
api_constants.LOG_LEVEL_WARNING);
|
||||
global_constants.LOG_LEVEL_WARNING);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
@@ -604,9 +639,9 @@ export default class BaseAPI {
|
||||
if (refObject === null || refObject === undefined) {
|
||||
if (!scorm2004) {
|
||||
if (attribute === '_children') {
|
||||
this.throwSCORMError(202);
|
||||
this.throwSCORMError(scorm12_error_codes.CHILDREN_ERROR);
|
||||
} else if (attribute === '_count') {
|
||||
this.throwSCORMError(203);
|
||||
this.throwSCORMError(scorm12_error_codes.COUNT_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -620,7 +655,7 @@ export default class BaseAPI {
|
||||
* @return {boolean}
|
||||
*/
|
||||
isInitialized() {
|
||||
return this.currentState === api_constants.STATE_INITIALIZED;
|
||||
return this.currentState === global_constants.STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -629,7 +664,7 @@ export default class BaseAPI {
|
||||
* @return {boolean}
|
||||
*/
|
||||
isNotInitialized() {
|
||||
return this.currentState === api_constants.STATE_NOT_INITIALIZED;
|
||||
return this.currentState === global_constants.STATE_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,7 +673,7 @@ export default class BaseAPI {
|
||||
* @return {boolean}
|
||||
*/
|
||||
isTerminated() {
|
||||
return this.currentState === api_constants.STATE_TERMINATED;
|
||||
return this.currentState === global_constants.STATE_TERMINATED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -702,7 +737,7 @@ export default class BaseAPI {
|
||||
}
|
||||
|
||||
this.apiLog('throwSCORMError', null, errorNumber + ': ' + message,
|
||||
api_constants.LOG_LEVEL_ERROR);
|
||||
global_constants.LOG_LEVEL_ERROR);
|
||||
|
||||
this.lastErrorCode = String(errorNumber);
|
||||
}
|
||||
@@ -713,11 +748,24 @@ export default class BaseAPI {
|
||||
* @param {string} success
|
||||
*/
|
||||
clearSCORMError(success: String) {
|
||||
if (success !== undefined && success !== api_constants.SCORM_FALSE) {
|
||||
if (success !== undefined && success !== global_constants.SCORM_FALSE) {
|
||||
this.lastErrorCode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to store the data to the LMS, logs data if no LMS configured
|
||||
* APIs that inherit BaseAPI should override this function
|
||||
*
|
||||
* @param {boolean} _calculateTotalTime
|
||||
* @return {string}
|
||||
* @abstract
|
||||
*/
|
||||
storeData(_calculateTotalTime) {
|
||||
throw new Error(
|
||||
'The storeData method has not been implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads CMI data from a JSON object.
|
||||
*
|
||||
@@ -733,6 +781,8 @@ export default class BaseAPI {
|
||||
|
||||
CMIElement = CMIElement || 'cmi';
|
||||
|
||||
this.startingData = json;
|
||||
|
||||
for (const key in json) {
|
||||
if ({}.hasOwnProperty.call(json, key) && json[key]) {
|
||||
const currentCMIElement = CMIElement + '.' + key;
|
||||
@@ -769,10 +819,22 @@ export default class BaseAPI {
|
||||
* @return {object}
|
||||
*/
|
||||
renderCMIToJSONObject() {
|
||||
const cmi = this.cmi;
|
||||
// Do we want/need to return fields that have no set value?
|
||||
// return JSON.stringify({ cmi }, (k, v) => v === undefined ? null : v, 2);
|
||||
return JSON.parse(JSON.stringify(cmi));
|
||||
return JSON.parse(this.renderCMIToJSONString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cmi object to the proper format for LMS commit
|
||||
* APIs that inherit BaseAPI should override this function
|
||||
*
|
||||
* @param {boolean} _terminateCommit
|
||||
* @return {*}
|
||||
* @abstract
|
||||
*/
|
||||
renderCommitCMI(_terminateCommit) {
|
||||
throw new Error(
|
||||
'The storeData method has not been implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -783,6 +845,33 @@ export default class BaseAPI {
|
||||
this.cmi.getCurrentTotalTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the request to the LMS
|
||||
* @param {string} url
|
||||
* @param {object|Array} params
|
||||
* @return {object}
|
||||
*/
|
||||
processHttpRequest(url: String, params) {
|
||||
const httpReq = new XMLHttpRequest();
|
||||
httpReq.open('POST', url, false);
|
||||
httpReq.setRequestHeader('Content-Type',
|
||||
'application/x-www-form-urlencoded');
|
||||
try {
|
||||
if (params instanceof Array) {
|
||||
httpReq.send(params.join('&'));
|
||||
} else {
|
||||
httpReq.send(params);
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
'result': global_constants.SCORM_FALSE,
|
||||
'errorCode': this.#error_codes.GENERAL,
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.parse(httpReq.responseText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a SCORM error
|
||||
*
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
CMIObjectivesObject,
|
||||
} from './cmi/scorm12_cmi';
|
||||
import * as Utilities from './utilities';
|
||||
import {scorm12_constants} from './constants/api_constants';
|
||||
import {global_constants, scorm12_constants} from './constants/api_constants';
|
||||
import {scorm12_error_codes} from './constants/error_codes';
|
||||
import {scorm12_regex} from './constants/regex';
|
||||
|
||||
const constants = scorm12_constants;
|
||||
|
||||
@@ -20,9 +19,16 @@ const constants = scorm12_constants;
|
||||
export default class Scorm12API extends BaseAPI {
|
||||
/**
|
||||
* Constructor for SCORM 1.2 API
|
||||
* @param {object} settings
|
||||
*/
|
||||
constructor() {
|
||||
super(scorm12_error_codes);
|
||||
constructor(settings: {}) {
|
||||
const finalSettings = {
|
||||
...{
|
||||
mastery_override: false,
|
||||
}, ...settings,
|
||||
};
|
||||
|
||||
super(scorm12_error_codes, finalSettings);
|
||||
|
||||
this.cmi = new CMI();
|
||||
// Rename functions to match 1.2 Spec and expose to modules
|
||||
@@ -149,9 +155,11 @@ export default class Scorm12API extends BaseAPI {
|
||||
|
||||
if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) {
|
||||
newChild = new CMIObjectivesObject();
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement,
|
||||
'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
|
||||
newChild = new CMIInteractionsCorrectResponsesObject();
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement,
|
||||
'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
|
||||
newChild = new CMIInteractionsObjectivesObject();
|
||||
} else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) {
|
||||
newChild = new CMIInteractionsObject();
|
||||
@@ -201,4 +209,86 @@ export default class Scorm12API extends BaseAPI {
|
||||
// Data Model
|
||||
this.cmi = newAPI.cmi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cmi object to the proper format for LMS commit
|
||||
*
|
||||
* @param {boolean} terminateCommit
|
||||
* @return {object|Array}
|
||||
*/
|
||||
renderCommitCMI(terminateCommit: boolean) {
|
||||
const cmiExport = this.renderCMIToJSONObject();
|
||||
|
||||
if (terminateCommit) {
|
||||
cmiExport.cmi.core.total_time = this.getCurrentTotalTime();
|
||||
}
|
||||
|
||||
const result = [];
|
||||
const flattened = Utilities.flatten(cmiExport);
|
||||
switch (this.settings.dataCommitFormat) {
|
||||
case 'flattened':
|
||||
return Utilities.flatten(cmiExport);
|
||||
case 'params':
|
||||
for (const item in flattened) {
|
||||
if ({}.hasOwnProperty.call(flattened, item)) {
|
||||
result.push(`${item}=${flattened[item]}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
case 'json':
|
||||
default:
|
||||
return cmiExport;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to store the data to the LMS
|
||||
*
|
||||
* @param {boolean} terminateCommit
|
||||
* @return {string}
|
||||
*/
|
||||
storeData(terminateCommit: boolean) {
|
||||
if (terminateCommit) {
|
||||
const originalStatus = this.cmi.core.lesson_status;
|
||||
if (originalStatus === 'not attempted') {
|
||||
this.cmi.core.lesson_status = 'completed';
|
||||
}
|
||||
|
||||
if (this.cmi.core.lesson_mode === 'normal') {
|
||||
if (this.cmi.core.credit === 'credit') {
|
||||
if (this.settings.mastery_override &&
|
||||
this.cmi.student_data.mastery_score !== '' &&
|
||||
this.cmi.core.score.raw !== '') {
|
||||
if (parseFloat(this.cmi.core.score.raw) >=
|
||||
parseFloat(this.cmi.student_data.mastery_score)) {
|
||||
this.cmi.core.lesson_status = 'passed';
|
||||
} else {
|
||||
this.cmi.core.lesson_status = 'failed';
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.cmi.core.lesson_mode === 'browse') {
|
||||
if ((this.startingData?.cmi?.core?.lesson_status || '') === '' &&
|
||||
originalStatus === 'not attempted') {
|
||||
this.cmi.core.lesson_status = 'browsed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commitObject = this.renderCommitCMI(terminateCommit);
|
||||
|
||||
if (this.settings.lmsCommitUrl) {
|
||||
if (this.apiLogLevel === global_constants.LOG_LEVEL_DEBUG) {
|
||||
console.debug('Commit (terminated: ' +
|
||||
(terminateCommit ? 'yes' : 'no') + '): ');
|
||||
console.debug(commitObject);
|
||||
}
|
||||
return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject);
|
||||
} else {
|
||||
console.log('Commit (terminated: ' +
|
||||
(terminateCommit ? 'yes' : 'no') + '): ');
|
||||
console.log(commitObject);
|
||||
return global_constants.SCORM_TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
CMIInteractionsObjectivesObject,
|
||||
CMIObjectivesObject,
|
||||
} from './cmi/scorm2004_cmi';
|
||||
import * as Util from './utilities';
|
||||
import {scorm2004_constants} from './constants/api_constants';
|
||||
import * as Utilities from './utilities';
|
||||
import {global_constants, scorm2004_constants} from './constants/api_constants';
|
||||
import {scorm2004_error_codes} from './constants/error_codes';
|
||||
import {correct_responses} from './constants/response_constants';
|
||||
import {valid_languages} from './constants/language_constants';
|
||||
@@ -26,9 +26,16 @@ export default class Scorm2004API extends BaseAPI {
|
||||
|
||||
/**
|
||||
* Constructor for SCORM 2004 API
|
||||
* @param {object} settings
|
||||
*/
|
||||
constructor() {
|
||||
super(scorm2004_error_codes);
|
||||
constructor(settings: {}) {
|
||||
const finalSettings = {
|
||||
...{
|
||||
mastery_override: false,
|
||||
}, ...settings,
|
||||
};
|
||||
|
||||
super(scorm2004_error_codes, finalSettings);
|
||||
|
||||
this.cmi = new CMI();
|
||||
this.adl = new ADL();
|
||||
@@ -146,7 +153,8 @@ export default class Scorm2004API extends BaseAPI {
|
||||
|
||||
if (this.stringMatches(CMIElement, 'cmi\\.objectives\\.\\d')) {
|
||||
newChild = new CMIObjectivesObject();
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement,
|
||||
'cmi\\.interactions\\.\\d\\.correct_responses\\.\\d')) {
|
||||
const parts = CMIElement.split('.');
|
||||
const index = Number(parts[2]);
|
||||
const interaction = this.cmi.interactions.childArray[index];
|
||||
@@ -183,13 +191,16 @@ export default class Scorm2004API extends BaseAPI {
|
||||
if (this.lastErrorCode === 0) {
|
||||
newChild = new CMIInteractionsCorrectResponsesObject();
|
||||
}
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
|
||||
} else if (foundFirstIndex && this.stringMatches(CMIElement,
|
||||
'cmi\\.interactions\\.\\d\\.objectives\\.\\d')) {
|
||||
newChild = new CMIInteractionsObjectivesObject();
|
||||
} else if (this.stringMatches(CMIElement, 'cmi\\.interactions\\.\\d')) {
|
||||
newChild = new CMIInteractionsObject();
|
||||
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_learner\\.\\d')) {
|
||||
} else if (this.stringMatches(CMIElement,
|
||||
'cmi\\.comments_from_learner\\.\\d')) {
|
||||
newChild = new CMICommentsObject();
|
||||
} else if (this.stringMatches(CMIElement, 'cmi\\.comments_from_lms\\.\\d')) {
|
||||
} else if (this.stringMatches(CMIElement,
|
||||
'cmi\\.comments_from_lms\\.\\d')) {
|
||||
newChild = new CMICommentsObject(true);
|
||||
}
|
||||
|
||||
@@ -420,4 +431,94 @@ export default class Scorm2004API extends BaseAPI {
|
||||
this.cmi = newAPI.cmi;
|
||||
this.adl = newAPI.adl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cmi object to the proper format for LMS commit
|
||||
*
|
||||
* @param {boolean} terminateCommit
|
||||
* @return {object|Array}
|
||||
*/
|
||||
renderCommitCMI(terminateCommit: boolean) {
|
||||
const cmi = this.renderCMIToJSONObject();
|
||||
|
||||
if (terminateCommit) {
|
||||
cmi.total_time = this.getCurrentTotalTime();
|
||||
}
|
||||
|
||||
const result = [];
|
||||
const flattened = Utilities.flatten(cmi);
|
||||
switch (this.settings.dataCommitFormat) {
|
||||
case 'flattened':
|
||||
return Utilities.flatten(cmi);
|
||||
case 'params':
|
||||
for (const item in flattened) {
|
||||
if ({}.hasOwnProperty.call(flattened, item)) {
|
||||
result.push(`${item}=${flattened[item]}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
case 'json':
|
||||
default:
|
||||
return cmi;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to store the data to the LMS
|
||||
*
|
||||
* @param {boolean} terminateCommit
|
||||
* @return {string}
|
||||
*/
|
||||
storeData(terminateCommit: boolean) {
|
||||
if (terminateCommit) {
|
||||
if (this.cmi.lesson_mode === 'normal') {
|
||||
if (this.cmi.credit === 'credit') {
|
||||
if (this.cmi.completion_threshold && this.cmi.progress_measure) {
|
||||
if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
|
||||
this.cmi.completion_status = 'completed';
|
||||
} else {
|
||||
this.cmi.completion_status = 'incomplete';
|
||||
}
|
||||
}
|
||||
if (this.cmi.scaled_passing_score !== null &&
|
||||
this.cmi.score.scaled !== '') {
|
||||
if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
|
||||
this.cmi.success_status = 'passed';
|
||||
} else {
|
||||
this.cmi.success_status = 'failed';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let navRequest = false;
|
||||
if (this.adl.nav.request !== (this.startingData?.adl?.nav?.request || '')) {
|
||||
this.adl.nav.request = encodeURIComponent(this.adl.nav.request);
|
||||
navRequest = true;
|
||||
}
|
||||
|
||||
const commitObject = this.renderCommitCMI(terminateCommit);
|
||||
|
||||
if (this.settings.lmsCommitUrl) {
|
||||
if (this.apiLogLevel === global_constants.LOG_LEVEL_DEBUG) {
|
||||
console.debug('Commit (terminated: ' +
|
||||
(terminateCommit ? 'yes' : 'no') + '): ');
|
||||
console.debug(commitObject);
|
||||
}
|
||||
const result = this.processHttpRequest(this.settings.lmsCommitUrl,
|
||||
commitObject);
|
||||
// check if this is a sequencing call, and then call the necessary JS
|
||||
if (navRequest && result.navRequest !== undefined &&
|
||||
result.navRequest !== '') {
|
||||
Function(`"use strict";(() => { ${result.navRequest} })()`)();
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
console.log('Commit (terminated: ' +
|
||||
(terminateCommit ? 'yes' : 'no') + '): ');
|
||||
console.log(commitObject);
|
||||
return global_constants.SCORM_TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ class CMICore extends BaseCMI {
|
||||
#student_name = '';
|
||||
#lesson_location = '';
|
||||
#credit = '';
|
||||
#lesson_status = '';
|
||||
#lesson_status = 'not attempted';
|
||||
#entry = '';
|
||||
#total_time = '';
|
||||
#lesson_mode = 'normal';
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
// @flow
|
||||
|
||||
export const global_constants = {
|
||||
SCORM_TRUE: 'true',
|
||||
SCORM_FALSE: 'false',
|
||||
STATE_NOT_INITIALIZED: 0,
|
||||
STATE_INITIALIZED: 1,
|
||||
STATE_TERMINATED: 2,
|
||||
LOG_LEVEL_DEBUG: 1,
|
||||
LOG_LEVEL_INFO: 2,
|
||||
LOG_LEVEL_WARNING: 3,
|
||||
LOG_LEVEL_ERROR: 4,
|
||||
LOG_LEVEL_NONE: 5,
|
||||
};
|
||||
|
||||
export const scorm12_constants = {
|
||||
// Children lists
|
||||
cmi_children: 'core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions',
|
||||
|
||||
@@ -157,3 +157,66 @@ export function addHHMMSSTimeStrings(
|
||||
const secondSeconds = getTimeAsSeconds(second, timeRegex);
|
||||
return getSecondsAsHHMMSS(firstSeconds + secondSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a JSON object down to string paths for each values
|
||||
* @param {object} data
|
||||
* @return {object}
|
||||
*/
|
||||
export function flatten(data) {
|
||||
const result = {};
|
||||
|
||||
/**
|
||||
* Recurse through the object
|
||||
* @param {*} cur
|
||||
* @param {*} prop
|
||||
*/
|
||||
function recurse(cur, prop) {
|
||||
if (Object(cur) !== cur) {
|
||||
result[prop] = cur;
|
||||
} else if (Array.isArray(cur)) {
|
||||
for (let i = 0, l = cur.length; i < l; i++) {
|
||||
recurse(cur[i], prop + '[' + i + ']');
|
||||
if (l === 0) result[prop] = [];
|
||||
}
|
||||
} else {
|
||||
let isEmpty = true;
|
||||
for (const p in cur) {
|
||||
if ({}.hasOwnProperty.call(cur, p)) {
|
||||
isEmpty = false;
|
||||
recurse(cur[p], prop ? prop + '.' + p : p);
|
||||
}
|
||||
}
|
||||
if (isEmpty && prop) result[prop] = {};
|
||||
}
|
||||
}
|
||||
|
||||
recurse(data, '');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-flatten a flat JSON object
|
||||
* @param {object} data
|
||||
* @return {object}
|
||||
*/
|
||||
export function unflatten(data) {
|
||||
'use strict';
|
||||
if (Object(data) !== data || Array.isArray(data)) return data;
|
||||
const regex = /\.?([^.[\]]+)|\[(\d+)]/g;
|
||||
const result = {};
|
||||
for (const p in data) {
|
||||
if ({}.hasOwnProperty.call(data, p)) {
|
||||
let cur = result;
|
||||
let prop = '';
|
||||
let m = regex.exec(p);
|
||||
while (m) {
|
||||
cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
|
||||
prop = m[2] || m[1];
|
||||
m = regex.exec(p);
|
||||
}
|
||||
cur[prop] = data[p];
|
||||
}
|
||||
}
|
||||
return result[''] || result;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import Scorm12API from '../src/Scorm12API';
|
||||
import * as h from './api_helpers';
|
||||
import {scorm12_error_codes} from '../src/constants/error_codes';
|
||||
|
||||
const api = () => {
|
||||
const API = new Scorm12API();
|
||||
const api = (settings = {}) => {
|
||||
const API = new Scorm12API(settings);
|
||||
API.apiLogLevel = 1;
|
||||
return API;
|
||||
};
|
||||
const apiInitialized = () => {
|
||||
const API = api();
|
||||
const apiInitialized = (settings = {}) => {
|
||||
const API = api(settings);
|
||||
API.lmsInitialize();
|
||||
return API;
|
||||
};
|
||||
@@ -271,4 +271,64 @@ describe('SCORM 1.2 API Tests', () => {
|
||||
firstAPI.cmi.core.student_id,
|
||||
).to.equal('student_2');
|
||||
});
|
||||
|
||||
describe('storeData()', () => {
|
||||
it('should set cmi.core.lesson_status to "completed"', () => {
|
||||
const scorm12API = api();
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('completed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "browsed"', () => {
|
||||
const scorm12API = api();
|
||||
scorm12API.cmi.core.lesson_mode = 'browse';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('browsed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "browsed" - Initial Status',
|
||||
() => {
|
||||
const scorm12API = api();
|
||||
scorm12API.startingData = {'cmi': {'core': {'lesson_status': ''}}};
|
||||
scorm12API.cmi.core.lesson_mode = 'browse';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('browsed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "passed" - mastery_override: true',
|
||||
() => {
|
||||
const scorm12API = api({mastery_override: true});
|
||||
scorm12API.cmi.core.credit = 'credit';
|
||||
scorm12API.cmi.student_data.mastery_score = '60.0';
|
||||
scorm12API.cmi.core.score.raw = '75.0';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('passed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "failed" - mastery_override: true',
|
||||
() => {
|
||||
const scorm12API = api({mastery_override: true});
|
||||
scorm12API.cmi.core.credit = 'credit';
|
||||
scorm12API.cmi.student_data.mastery_score = '60.0';
|
||||
scorm12API.cmi.core.score.raw = '55.0';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('failed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "passed" - mastery_override: false',
|
||||
() => {
|
||||
const scorm12API = api({mastery_override: false});
|
||||
scorm12API.cmi.core.lesson_status = 'failed'; // module author wanted the user to pass, so we don't override
|
||||
scorm12API.cmi.core.credit = 'credit';
|
||||
scorm12API.cmi.student_data.mastery_score = '60.0';
|
||||
scorm12API.cmi.core.score.raw = '75.0';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('failed');
|
||||
});
|
||||
it('should set cmi.core.lesson_status to "failed" - mastery_override: false',
|
||||
() => {
|
||||
const scorm12API = api({mastery_override: false});
|
||||
scorm12API.cmi.core.lesson_status = 'passed'; // module author wanted the user to pass, so we don't override
|
||||
scorm12API.cmi.core.credit = 'credit';
|
||||
scorm12API.cmi.student_data.mastery_score = '60.0';
|
||||
scorm12API.cmi.core.score.raw = '55.0';
|
||||
scorm12API.storeData(true);
|
||||
expect(scorm12API.cmi.core.lesson_status).to.equal('passed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,7 @@ describe('AICC CMI Tests', () => {
|
||||
h.checkRead({
|
||||
cmi: cmi(),
|
||||
fieldName: 'cmi.core.lesson_status',
|
||||
expectedValue: 'not attempted',
|
||||
});
|
||||
h.checkValidValues({
|
||||
cmi: cmi(),
|
||||
@@ -334,6 +335,7 @@ describe('AICC CMI Tests', () => {
|
||||
h.checkRead({
|
||||
cmi: cmi(),
|
||||
fieldName: 'cmi.core.lesson_status',
|
||||
expectedValue: 'not attempted',
|
||||
});
|
||||
h.checkValidValues({
|
||||
cmi: cmi(),
|
||||
@@ -517,7 +519,7 @@ describe('AICC CMI Tests', () => {
|
||||
).
|
||||
to.
|
||||
equal(
|
||||
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":"","tries":{"0":{"status":"","time":"","score":{"raw":"","min":"","max":"100"}}}},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}},"evaluation":{"comments":{"0":{"content":"","location":"","time":""}}}}');
|
||||
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":"","tries":{"0":{"status":"","time":"","score":{"raw":"","min":"","max":"100"}}}},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}},"evaluation":{"comments":{"0":{"content":"","location":"","time":""}}}}');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ describe('SCORM 1.2 CMI Tests', () => {
|
||||
h.checkRead({
|
||||
cmi: cmi(),
|
||||
fieldName: 'cmi.core.lesson_status',
|
||||
expectedValue: 'not attempted',
|
||||
});
|
||||
h.checkValidValues({
|
||||
cmi: cmi(),
|
||||
@@ -415,6 +416,7 @@ describe('SCORM 1.2 CMI Tests', () => {
|
||||
h.checkRead({
|
||||
cmi: cmiInitialized(),
|
||||
fieldName: 'cmi.core.lesson_status',
|
||||
expectedValue: 'not attempted',
|
||||
});
|
||||
h.checkValidValues({
|
||||
cmi: cmiInitialized(),
|
||||
@@ -590,7 +592,7 @@ describe('SCORM 1.2 CMI Tests', () => {
|
||||
).
|
||||
to.
|
||||
equal(
|
||||
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}');
|
||||
'{"suspend_data":"","launch_data":"","comments":"","comments_from_lms":"","core":{"student_id":"","student_name":"","lesson_location":"","credit":"","lesson_status":"not attempted","entry":"","total_time":"","lesson_mode":"normal","exit":"","session_time":"00:00:00","score":{"raw":"","min":"","max":"100"}},"objectives":{"0":{"id":"","status":"","score":{"raw":"","min":"","max":"100"}}},"student_data":{"mastery_score":"","max_time_allowed":"","time_limit_action":""},"student_preference":{"audio":"","language":"","speed":"","text":""},"interactions":{"0":{"id":"","time":"","type":"","weighting":"","student_response":"","result":"","latency":"","objectives":{},"correct_responses":{}}}}');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -274,4 +274,60 @@ describe('Utility Tests', () => {
|
||||
).to.equal('01:05:30.5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flatten()', () => {
|
||||
it('Should return flattened object', () => {
|
||||
expect(
|
||||
Utilities.flatten({
|
||||
'cmi': {
|
||||
'core': {
|
||||
'learner_id': 'jputney',
|
||||
'learner_name': 'Jonathan',
|
||||
},
|
||||
'objectives': {
|
||||
'0': {
|
||||
'id': 'AAA',
|
||||
},
|
||||
'1': {
|
||||
'id': 'BBB',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.eql({
|
||||
'cmi.core.learner_id': 'jputney',
|
||||
'cmi.core.learner_name': 'Jonathan',
|
||||
'cmi.objectives.0.id': 'AAA',
|
||||
'cmi.objectives.1.id': 'BBB',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unflatten()', () => {
|
||||
it('Should return flattened object', () => {
|
||||
expect(
|
||||
Utilities.unflatten({
|
||||
'cmi.core.learner_id': 'jputney',
|
||||
'cmi.core.learner_name': 'Jonathan',
|
||||
'cmi.objectives.0.id': 'AAA',
|
||||
'cmi.objectives.1.id': 'BBB',
|
||||
}),
|
||||
).to.eql({
|
||||
'cmi': {
|
||||
'core': {
|
||||
'learner_id': 'jputney',
|
||||
'learner_name': 'Jonathan',
|
||||
},
|
||||
'objectives': {
|
||||
'0': {
|
||||
'id': 'AAA',
|
||||
},
|
||||
'1': {
|
||||
'id': 'BBB',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user