diff --git a/.DS_Store b/.DS_Store index 501cf9b..be5e202 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 0090352..eaa4dcf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ # TODO -1. set lesson status to done? when final button clicked `lesson_status`. -2. update attempts `cmi.core.lesson_status` add to done button. -3. calculate total time spent `session_time`. -4. replace golf references! -5. replace alerts with `console.error()`. +- Make sure we warn if score not in bounds. +- Display current score on page. +- display pass/fail onscreen. +- calculate total time spent `session_time`. + +## Notes + +- the `cmi.student_data` section appears to be readonly and trying to write causes issues. + +## Moodle SCORM Settings + +- Attempts Management: Force new attempt (when previous attempt completes, passes or fails). +- Mastery score overrides status (defaults to yes) means Moodle reports failed attempts. +- The mastery score (pass threshold) is stored in the manifest file (not reported by Moodle). diff --git a/imsmanifest.xml b/imsmanifest.xml index fd58d91..3aa0c98 100755 --- a/imsmanifest.xml +++ b/imsmanifest.xml @@ -10,7 +10,7 @@ score and time. It also includes the addition of a basic "controller" for provid intra-SCO navigation. --> -ADL SCORM 1.2 - - - Golf Explained - Run-time Basic Calls + + + Simple SCORM Template - Golf Explained + Template + 40 diff --git a/js/scormfunctions.js b/js/scormfunctions.js index aa38be9..21c03c9 100755 --- a/js/scormfunctions.js +++ b/js/scormfunctions.js @@ -1,3 +1,4 @@ + /* Source code created by Rustici Software, LLC is licensed under a Creative Commons Attribution 3.0 United States License @@ -15,136 +16,209 @@ some basic SCORM error handling. //Include the standard ADL-supplied API discovery algorithm +// list of writable SCORM data values: + +const scormData = { + cmi: { + core: { + student_id: { + readable: true, + description: 'student username', + datatype: 'string' + }, + student_name: { + readable: true, + description: 'student name', + datatype: 'string' + }, + lesson_location: { + readable: true, + writable: true, + description: 'bookmark', + datatype: 'string' + }, + credit: { + readable: true, + description: 'bookmark', + datatype: 'string', + options: [ + 'credit', + 'no-credit' + ] + }, + lesson_status: { + readable: true, + writable: true, + description: 'bookmark', + datatype: 'string', + options: [ + 'passed', + 'completed', + 'failed', + 'incomplete', + 'browsed', + 'not attempted' + ] + }, + entry: { + readable: true, + description: 'Asserts whether the learner has previously accessed the SCO', + datatype: 'string', + options: [ + 'ab-initio', + 'resume', + '' + ] + }, + score: { + raw: { + readable: true, + writable: true, + description: 'the performance of the learner', + datatype: 'number' + }, + max: { + readable: true, + writable: true, + description: 'Maximum value in the range for the raw score', + datatype: 'number' + }, + min: { + readable: true, + writable: true, + description: 'Minimum value in the range for the raw score', + datatype: 'number' + } + }, + total_time: { + readable: true, + description: 'Sum of all of the learner’s session times accumulated in the current learner attempt', + datatype: 'CMITimespan' + } + } + } +} /////////////////////////////////////////// //Begin ADL-provided API discovery algorithm /////////////////////////////////////////// -var findAPITries = 0; - -function findAPI(win) -{ - // Check to see if the window (win) contains the API - // if the window (win) does not contain the API and - // the window (win) has a parent window and the parent window - // is not the same as the window (win) - while ( (win.API == null) && - (win.parent != null) && - (win.parent != win) ) - { - // increment the number of findAPITries - findAPITries++; - - // Note: 7 is an arbitrary number, but should be more than sufficient - if (findAPITries > 7) - { - alert("Error finding API -- too deeply nested."); - return null; - } - - // set the variable that represents the window being - // being searched to be the parent of the current window - // then search for the API again - win = win.parent; - } - return win.API; -} +var findAPITries = 0 -function getAPI() -{ - // start by looking for the API in the current window - var theAPI = findAPI(window); - - // if the API is null (could not be found in the current window) - // and the current window has an opener window - if ( (theAPI == null) && - (window.opener != null) && - (typeof(window.opener) != "undefined") ) - { - // try to find the API in the current window�s opener - theAPI = findAPI(window.opener); - } - // if the API has not been found - if (theAPI == null) - { - // Alert the user that the API Adapter could not be found - alert("Unable to find an API adapter"); - } - return theAPI; +/** + * Check to see if the window (win) contains the API + if the window (win) does not contain the API and + the window (win) has a parent window and the parent window + is not the same as the window (win) + * @param {window} win the top-level window object + */ +function findAPI(win) { + while ( (win.API == null) && (win.parent != null) && (win.parent != win) ) { + // increment the number of findAPITries + findAPITries++ + // Note: 7 is an arbitrary number, but should be more than sufficient + if (findAPITries > 7) { + console.error('Error finding API -- too deeply nested.') + return null + } + // set the variable that represents the window being + // being searched to be the parent of the current window + // then search for the API again + win = win.parent + } + return win.API } +/** + * open the API and return it. + */ +function getAPI() { + // start by looking for the API in the current window + let theAPI = findAPI(window) + // if the API is null (could not be found in the current window) + // and the current window has an opener window + if ( (theAPI == null) && (window.opener != null) && (typeof(window.opener) != 'undefined') ) { + // try to find the API in the current window�s opener + theAPI = findAPI(window.opener) + } + // if the API has not been found + if (theAPI == null) { + // Alert the user that the API Adapter could not be found + console.error('Unable to find an API adapter') + } + return theAPI +} /////////////////////////////////////////// //End ADL-provided API discovery algorithm /////////////////////////////////////////// - - + +function MillisecondsToCMIDuration(ms) { + //Convert duration from milliseconds to 0000:00:00.00 format + var hms = '' + var dtm = new Date + dtm.setTime(ms) + var h = "000" + Math.floor(ms/3600000) + var m = "0" + dtm.getMinutes() + var s = "0" + dtm.getSeconds() + var cs = "0" + Math.round(dtm.getMilliseconds() / 10); hms = h.substr(h.length-4)+":"+m.substr(m.length-2)+":"; + hms += s.substr(s.length-2)+"."+cs.substr(cs.length-2); + return hms + } + //Create function handlers for the loading and unloading of the page //Constants -var SCORM_TRUE = "true"; -var SCORM_FALSE = "false"; -var SCORM_NO_ERROR = "0"; +const SCORM_TRUE = 'true' +const SCORM_FALSE = 'false' +const SCORM_NO_ERROR = '0' //Since the Unload handler will be called twice, from both the onunload //and onbeforeunload events, ensure that we only call LMSFinish once. -var finishCalled = false; +let finishCalled = false //Track whether or not we successfully initialized. -var initialized = false; - -var API = null; +let initialized = false +let API = null function ScormProcessInitialize(){ - var result; - - API = getAPI(); - - if (API == null){ - alert("ERROR - Could not establish a connection with the LMS.\n\nYour results may not be recorded."); - return; - } - - result = API.LMSInitialize(""); - - if (result == SCORM_FALSE){ - var errorNumber = API.LMSGetLastError(); - var errorString = API.LMSGetErrorString(errorNumber); - var diagnostic = API.LMSGetDiagnostic(errorNumber); - - var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic; - - alert("Error - Could not initialize communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription); - return; - } - - initialized = true; + var result + API = getAPI() + if (API == null) { + console.error('ERROR - Could not establish a connection with the LMS.') + return + } + result = API.LMSInitialize('') + + if (result == SCORM_FALSE) { + var errorNumber = API.LMSGetLastError() + var errorString = API.LMSGetErrorString(errorNumber) + var diagnostic = API.LMSGetDiagnostic(errorNumber) + var errorDescription = ` Number: ${errorNumber}\n Description: ${errorString}\n Diagnostic: ${diagnostic}` + console.error(`Error - Could not initialize communication with the LMS.\n${errorDescription}`) + return + } + initialized = true } -function ScormProcessFinish(){ - - var result; - - //Don't terminate if we haven't initialized or if we've already terminated - if (initialized == false || finishCalled == true){return;} - - result = API.LMSFinish(""); - - finishCalled = true; - - if (result == SCORM_FALSE){ - var errorNumber = API.LMSGetLastError(); - var errorString = API.LMSGetErrorString(errorNumber); - var diagnostic = API.LMSGetDiagnostic(errorNumber); - - var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic; - - alert("Error - Could not terminate communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription); - return; - } +function ScormProcessFinish() { + let result + //Don't terminate if we haven't initialized or if we've already terminated + if (initialized == false || finishCalled == true) { + return + } + result = API.LMSFinish('') + finishCalled = true + if (result == SCORM_FALSE) { + const errorNumber = API.LMSGetLastError() + const errorString = API.LMSGetErrorString(errorNumber) + const diagnostic = API.LMSGetDiagnostic(errorNumber) + const errorDescription = ` Number: ${errorNumber}\n Description: ${errorString}\n Diagnostic: ${diagnostic}` + console.error(`Error - Could not terminate communication with the LMS.\n${errorDescription}`) + return + } } - /* The onload and onunload event handlers are assigned in launchpage.html because more processing needs to occur at these times in this example. @@ -154,52 +228,44 @@ occur at these times in this example. //window.onbeforeunload = ScormProcessTerminate; -function ScormProcessGetValue(element){ - - var result; - - if (initialized == false || finishCalled == true){return;} - - result = API.LMSGetValue(element); - - if (result == ""){ - - var errorNumber = API.LMSGetLastError(); - - if (errorNumber != SCORM_NO_ERROR){ - var errorString = API.LMSGetErrorString(errorNumber); - var diagnostic = API.LMSGetDiagnostic(errorNumber); - - var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic; - - alert("Error - Could not retrieve a value from the LMS.\n\n" + errorDescription); - return ""; - } - } - - return result; +function ScormProcessGetValue(element) { + let result + if(initialized == false || finishCalled == true) { + return + } + result = API.LMSGetValue(element) + if (result == '') { + const errorNumber = API.LMSGetLastError() + if (errorNumber != SCORM_NO_ERROR) { + const errorString = API.LMSGetErrorString(errorNumber) + const diagnostic = API.LMSGetDiagnostic(errorNumber) + const errorDescription = ` Number: ${errorNumber}\n Description: ${errorString}\n Diagnostic: ${diagnostic}` + console.error(`Error - Could not retrieve a value from the LMS.\n${errorDescription}`) + return '' + } + } + return result } function ScormProcessSetValue(element, value) { - - var result; - - if (initialized == false || finishCalled == true){return;} - - result = API.LMSSetValue(element, value) - console.log(result) - console.log('value set') - API.LMSCommit('') - console.log('data committed') - if (result == SCORM_FALSE){ - var errorNumber = API.LMSGetLastError(); - var errorString = API.LMSGetErrorString(errorNumber); - var diagnostic = API.LMSGetDiagnostic(errorNumber); - - var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic; - - alert("Error - Could not store a value in the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription); - return; - } - -} \ No newline at end of file + let result + if (initialized == false || finishCalled == true) { + return + } + if(element.includes('cmi.student_data')) { + console.warn(`element ${element} is readonly. value not saved.`) + return + } + result = API.LMSSetValue(element, value) + console.log(result) + API.LMSCommit('') + console.log('data committed') + if (result == SCORM_FALSE) { + const errorNumber = API.LMSGetLastError() + const errorString = API.LMSGetErrorString(errorNumber) + const diagnostic = API.LMSGetDiagnostic(errorNumber) + const errorDescription = ` Number: ${errorNumber}\n Description: ${errorString}\n Diagnostic: ${diagnostic}` + console.error(`Error - Could not store a value in the LMS.\n${errorDescription}`) + return + } +} diff --git a/js/script.js b/js/script.js index 7e23247..eca9224 100644 --- a/js/script.js +++ b/js/script.js @@ -3,34 +3,57 @@ document.addEventListener('DOMContentLoaded', event => { console.log('DOM CONTENT LOADED') document.querySelector('button#update').addEventListener('click', event => { console.log('UPDATE BUTTON CLICKED') - const score = document.querySelector('input').value - console.log(`score: ${score}`) - ScormProcessSetValue("cmi.core.score.raw", parseInt(score)) - ScormProcessSetValue("cmi.core.score.min", "0") - ScormProcessSetValue("cmi.core.score.max", "100") + const score = parseInt(document.querySelector('input').value) + updateScore(score) }) document.querySelector('button#end').addEventListener('click', event => { console.log('END BUTTON CLICKED') + endGame(parseInt(document.querySelector('input').value)) ScormProcessFinish() }) }) +function updateScore(score) { + score = parseInt(score) + console.log(`score: ${score}`) + ScormProcessSetValue('cmi.core.score.raw', parseInt(score)) +} + +function endGame(score) { + updateScore(score) + // set the status + const mastery = parseInt(ScormProcessGetValue('cmi.student_data.mastery_score')) + console.log(`mastery: (${mastery})`) + if(!isNaN(mastery)) { // NaN if mastery score not provided by the API + const status = parseInt(score) >= mastery ? 'passed' : 'failed' // if mastery score then pass/fail + console.log(`status: (${status})`) + ScormProcessSetValue('cmi.core.lesson_status', status) + } else { + console.log('status is: (completed)') + ScormProcessSetValue('cmi.core.lesson_status', 'completed') // if no mastery then completed + } +} + window.addEventListener('load', event => { console.log('LOAD') ScormProcessInitialize() - const completionStatus = ScormProcessGetValue("cmi.core.lesson_status") - if (completionStatus == "not attempted") { + const completionStatus = ScormProcessGetValue('cmi.core.lesson_status') + if (completionStatus == 'not attempted') { console.log('not attempted') - ScormProcessSetValue("cmi.core.lesson_status", "incomplete") + ScormProcessSetValue('cmi.core.lesson_status', 'incomplete') } const name = ScormProcessGetValue('cmi.core.student_name') - console.log(name) + console.log(`student name: ${name}`) document.querySelector('p').innerText = name + const username = ScormProcessGetValue('cmi.core.student_id') + console.log(`student ID: ${username}`) + ScormProcessSetValue('cmi.core.score.min', 0) + ScormProcessSetValue('cmi.core.score.max', 100) }) window.addEventListener('beforeunload', event => { console.log('BEFOREUNLOAD') - ScormProcessSetValue("cmi.core.exit", "") + ScormProcessSetValue('cmi.core.exit', '') ScormProcessFinish() }) diff --git a/template 1.9.zip b/template 1.9.zip new file mode 100644 index 0000000..5c6549b Binary files /dev/null and b/template 1.9.zip differ