Working on the saving of data

This commit is contained in:
Jonathan Putney
2019-11-15 17:24:30 -05:00
parent 51da89f737
commit 8e6f6c47e9
11 changed files with 559 additions and 75 deletions

View File

@@ -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();

View File

@@ -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
*

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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';

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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');
});
});
});

View File

@@ -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":""}}}}');
});
});

View File

@@ -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":{}}}}');
});
});

View File

@@ -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',
},
},
},
});
});
});
});