From 6e2294df3474e10d7d0b134fc176b44fccdfafe9 Mon Sep 17 00:00:00 2001 From: Mark Tyers Date: Thu, 4 Jun 2020 11:25:32 +0100 Subject: [PATCH] package reports status --- .DS_Store | Bin 6148 -> 6148 bytes README.md | 19 ++- imsmanifest.xml | 11 +- js/scormfunctions.js | 366 +++++++++++++++++++++++++------------------ js/script.js | 43 +++-- template 1.9.zip | Bin 0 -> 16127 bytes 6 files changed, 269 insertions(+), 170 deletions(-) create mode 100644 template 1.9.zip diff --git a/.DS_Store b/.DS_Store index 501cf9b9dd19767f6753eed930a853030c97392b..be5e202f0e29a05259b9665a48d42abb9282b8f0 100644 GIT binary patch delta 72 zcmZoMXfc@J&&abeU^g=(&tx7JcWrJ4M}{JXWQGieOolRsR0chUDj=)CGbcYeDJMUP Xfq_8)h))8s$7T)g{N)D#o7f58 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 0000000000000000000000000000000000000000..5c6549b792fbaec7e0d04a2c00ee075cdc94b0a6 GIT binary patch literal 16127 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