From 8bc07c875b03b7ded474f7bbaf09d7a46ed4e03e Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Tue, 4 Jul 2023 10:24:03 +0100 Subject: [PATCH] move loadVariables from SCORMStorage to BlankStorage The `loadVariables` method doesn't depend on the SCORM API, so needs to be in the generic storage. This commit also updates the part unit tests to use the generic storage object, instead of just a SCORM storage (which is why the above bug wasn't caught!) --- runtime/scripts/scorm-storage.js | 22 ----- runtime/scripts/storage.js | 24 ++++++ tests/jme/doc-tests.mjs | 6 +- tests/numbas-runtime.js | 137 ++++++++++++++++++++++--------- tests/parts/part-tests.mjs | 55 +++++++------ tests/parts/scorm_api.mjs | 1 + 6 files changed, 157 insertions(+), 88 deletions(-) diff --git a/runtime/scripts/scorm-storage.js b/runtime/scripts/scorm-storage.js index 9debc3d5e..b8f5449c9 100644 --- a/runtime/scripts/scorm-storage.js +++ b/runtime/scripts/scorm-storage.js @@ -350,28 +350,6 @@ SCORMStorage.prototype = /** @lends Numbas.storage.SCORMStorage.prototype */ { }; }, - /** Load a dictionary of JME variables. - * - * @param {Object} vobj - * @param {Numbas.jme.Scope} scope - * @returns {Object} - */ - loadVariables: function(vobj, scope) { - var variables = {}; - for(var snames in vobj) { - var v = scope.evaluate(vobj[snames]); - var names = snames.split(','); - if(names.length>1) { - names.forEach(function(name,i) { - variables[name] = scope.evaluate('$multi['+i+']',{'$multi':v}); - }); - } else { - variables[snames] = v; - } - } - return variables; - }, - /** Get suspended info for a question. * * @param {Numbas.Question} question diff --git a/runtime/scripts/storage.js b/runtime/scripts/storage.js index 28e06cf4d..e4506c292 100644 --- a/runtime/scripts/storage.js +++ b/runtime/scripts/storage.js @@ -160,6 +160,30 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p * @returns {Numbas.storage.part_suspend_data} */ loadExtensionPart: function(part) {}, + + /** Load a dictionary of JME variables. + * + * @param {Object} vobj + * @param {Numbas.jme.Scope} scope + * @returns {Object} + */ + loadVariables: function(vobj, scope) { + var variables = {}; + for(var snames in vobj) { + var v = scope.evaluate(vobj[snames]); + var names = snames.split(','); + if(names.length>1) { + names.forEach(function(name,i) { + variables[name] = scope.evaluate('$multi['+i+']',{'$multi':v}); + }); + } else { + variables[snames] = v; + } + } + return variables; + }, + + /** Call this when the exam is started (when {@link Numbas.Exam#begin} runs, not when the page loads). * * @abstract diff --git a/tests/jme/doc-tests.mjs b/tests/jme/doc-tests.mjs index 4c2ebd2a6..09b0dffcb 100644 --- a/tests/jme/doc-tests.mjs +++ b/tests/jme/doc-tests.mjs @@ -1905,7 +1905,7 @@ export default ], "noexamples": false, "calling_patterns": [ - "trunc(x)" + "trunc(x, [p])" ], "examples": [ { @@ -1915,6 +1915,10 @@ export default { "in": "trunc(-3.3)", "out": "-3" + }, + { + "in": "trunc(9.8765, 2)", + "out": "9.87" } ] }, diff --git a/tests/numbas-runtime.js b/tests/numbas-runtime.js index d7ff4fca3..a74006ad4 100644 --- a/tests/numbas-runtime.js +++ b/tests/numbas-runtime.js @@ -438,10 +438,9 @@ var util = Numbas.util = /** @lends Numbas.util */ { */ extend: function(a,b,extendMethods) { - var c = function() - { + var c = function() { a.apply(this,arguments); - b.apply(this,arguments); + return b.apply(this,arguments); }; var x; for(x in a.prototype) @@ -1140,6 +1139,19 @@ var util = Numbas.util = /** @lends Numbas.util */ { var d = parseInt(m[4]); return {numerator:n, denominator:d}; }, + + /** Transform the given string to one containing only letters, digits and hyphens. + * @param {string} str + * @returns {string} + */ + slugify: function(str) { + if (str === undefined){ + return ''; + } + return (str + '').replace(/\s+/g,'-').replace(/[^a-zA-Z0-9\-]/g,'').replace(/-+/g,'-'); + + }, + /** Pad string `s` on the left with a character `p` until it is `n` characters long. * * @param {string} s @@ -1722,7 +1734,31 @@ var util = Numbas.util = /** @lends Numbas.util */ { cb = fn; go(); } - } + }, + + /** Encode the contents of an ArrayBuffer in base64. + * + * @param {ArrayBuffer} arrayBuffer + * @returns {string} + */ + b64encode: function (arrayBuffer) { + return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) + }, + + /** Decode a base64 string to an ArrayBuffer. + * + * @param {string} encoded + * @returns {ArrayBuffer} + */ + b64decode: function (encoded) { + let byteString = atob(encoded); + const bytes = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; i++) { + bytes[i] = byteString.charCodeAt(i); + } + return bytes.buffer; + }, + }; /** @@ -18617,8 +18653,8 @@ jme.variables = /** @lends Numbas.jme.variables */ { definitions.forEach(function(def) { var names = def.name.split(/\s*,\s*/); var value = def.value; - if(typeof value == 'string') { - value = scope.evaluate(value); + if(typeof value != 'object') { + value = scope.evaluate(value+''); } names.forEach(function(name) { defined_names.push(jme.normaliseName(name,scope)); @@ -23140,7 +23176,11 @@ Exam.prototype = /** @lends Numbas.Exam.prototype */ { this.xml = xml; tryGetAttribute(settings,xml,'.',['name','percentPass','allowPrinting']); tryGetAttribute(settings,xml,'questions',['shuffle','all','pick'],['shuffleQuestions','allQuestions','pickQuestions']); - tryGetAttribute(settings,xml,'settings/navigation',['allowregen','navigatemode','reverse','browse','allowsteps','showfrontpage','showresultspage','preventleave','startpassword'],['allowRegen','navigateMode','navigateReverse','navigateBrowse','allowSteps','showFrontPage','showResultsPage','preventLeave','startPassword']); + tryGetAttribute(settings, + xml, + 'settings/navigation', + ['allowregen','navigatemode','reverse','browse','allowsteps','showfrontpage','showresultspage','preventleave','startpassword','allowAttemptDownload','downloadEncryptionKey'], + ['allowRegen','navigateMode','navigateReverse','navigateBrowse','allowSteps','showFrontPage','showResultsPage','preventLeave','startPassword','allowAttemptDownload','downloadEncryptionKey']); //get navigation events and actions var navigationEventNodes = xml.selectNodes('settings/navigation/event'); for( var i=0; i} vobj - * @param {Numbas.jme.Scope} scope - * @returns {Object} - */ - loadVariables: function(vobj, scope) { - var variables = {}; - for(var snames in vobj) { - var v = scope.evaluate(vobj[snames]); - var names = snames.split(','); - if(names.length>1) { - names.forEach(function(name,i) { - variables[name] = scope.evaluate('$multi['+i+']',{'$multi':v}); - }); - } else { - variables[snames] = v; - } - } - return variables; - }, - /** Get suspended info for a question. * * @param {Numbas.Question} question @@ -26509,7 +26532,7 @@ SCORMStorage.prototype = /** @lends Numbas.storage.SCORMStorage.prototype */ { pobj = pobj.gaps[i]; break; case 's': - storage= pobj.steps[i]; + pobj = pobj.steps[i]; break; } } @@ -26860,7 +26883,9 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p * * @param {Numbas.Exam} exam */ - init: function(exam) {}, + init: function(exam) { + this.exam = exam; + }, /** Initialise a question. * * @param {Numbas.Question} q @@ -26957,6 +26982,30 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p * @returns {Numbas.storage.part_suspend_data} */ loadExtensionPart: function(part) {}, + + /** Load a dictionary of JME variables. + * + * @param {Object} vobj + * @param {Numbas.jme.Scope} scope + * @returns {Object} + */ + loadVariables: function(vobj, scope) { + var variables = {}; + for(var snames in vobj) { + var v = scope.evaluate(vobj[snames]); + var names = snames.split(','); + if(names.length>1) { + names.forEach(function(name,i) { + variables[name] = scope.evaluate('$multi['+i+']',{'$multi':v}); + }); + } else { + variables[snames] = v; + } + } + return variables; + }, + + /** Call this when the exam is started (when {@link Numbas.Exam#begin} runs, not when the page loads). * * @abstract @@ -27082,7 +27131,10 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p questionGroupOrder: exam.questionGroupOrder, start: exam.start-0, stop: exam.stop ? exam.stop-0 : null, - randomSeed: exam && exam.seed + randomSeed: exam && exam.seed, + student_name: exam.student_name, + score: exam.score, + max_score: exam.mark, }; if(exam.settings.navigateMode=='diagnostic') { eobj.diagnostic = this.diagnosticSuspendData(); @@ -27136,7 +27188,9 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p answered: question.answered, submitted: question.submitted, adviceDisplayed: question.adviceDisplayed, - revealed: question.revealed + revealed: question.revealed, + score: question.score, + max_score: question.marks }; var scope = question.getScope(); @@ -27217,8 +27271,15 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p return { pre_submit_cache: alt.pre_submit_cache.map(pre_submit_cache_suspendData) }; - }) + }), + score: part.score, + max_score: part.marks, }; + let partTypes = storage.partTypeStorage; + if (part.type != 'gapfill') { + pobj.student_answer = partTypes[part.type].student_answer(part); + pobj.correct_answer = partTypes[part.type].correct_answer(part); + } var typeStorage = this.getPartStorage(part); if(typeStorage) { var data = typeStorage.suspend_data(part, this); @@ -28616,7 +28677,7 @@ Numbas.queueScript('answer-widgets',['knockout','util','jme','jme-display'],func Numbas.parts.CustomPart.prototype.student_answer_jme_types[name] = params.answer_to_jme; var input_option_types = Numbas.parts.CustomPart.prototype.input_option_types[name] = {}; if(Numbas.storage) { - Numbas.storage.scorm.inputWidgetStorage[name] = params.scorm_storage; + Numbas.storage.inputWidgetStorage[name] = params.scorm_storage; } params.options_definition.forEach(function(def) { var types = { diff --git a/tests/parts/part-tests.mjs b/tests/parts/part-tests.mjs index addeb9acc..5f76f6169 100644 --- a/tests/parts/part-tests.mjs +++ b/tests/parts/part-tests.mjs @@ -2068,8 +2068,8 @@ mark: }; const [run1,run2] = await with_scorm( async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2077,8 +2077,8 @@ mark: }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2113,8 +2113,8 @@ mark: }; const run1 = await with_scorm( async function(data, results, scorm) { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); e.begin(); @@ -2174,8 +2174,8 @@ mark: const [run1,run2] = await with_scorm( async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2184,8 +2184,8 @@ mark: }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2220,8 +2220,8 @@ mark: const [run1,run2] = await with_scorm( async function(data, results, scorm) { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2232,8 +2232,8 @@ mark: }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2335,9 +2335,9 @@ mark: const [run1,run2] = await with_scorm( async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); Numbas.activateExtension('test_deterministic_variables'); - var e = Numbas.createExamFromJSON(exam_def,store,false); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2345,11 +2345,12 @@ mark: }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); + var store = new Numbas.storage.scorm.SCORMStorage(); + Numbas.storage.addStorage(store); var suspend = store.getSuspendData(); var qobj = suspend.questions[0]; assert.deepEqual(Object.keys(qobj.variables),['b','d','g','h','i','k'], 'Only non-deterministic variables are saved') - var e = Numbas.createExamFromJSON(exam_def,store,false); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); const q = e.questionList[0]; @@ -2401,16 +2402,16 @@ mark: const [run1,run2] = await with_scorm( async function(data, results, scorm) { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); return true; }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); return true; @@ -2466,20 +2467,20 @@ mark: const [run1,run2] = await with_scorm( async function(data, results, scorm) { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.init(); await e.signals.on('ready'); return true; }, async function() { - var store = Numbas.store = new Numbas.storage.scorm.SCORMStorage(); - var e = Numbas.createExamFromJSON(exam_def,store,false); + Numbas.storage.addStorage(new Numbas.storage.scorm.SCORMStorage()); + var e = Numbas.createExamFromJSON(exam_def,Numbas.store,false); e.load(); await e.signals.on('ready'); const q = e.questionList[0]; - console.log(store.loadQuestion(q)); + console.log(Numbas.store.loadQuestion(q)); const a = q.scope.getVariable('a'); const b = q.scope.getVariable('b'); const c = q.scope.getVariable('c'); diff --git a/tests/parts/scorm_api.mjs b/tests/parts/scorm_api.mjs index bb72329d4..a23a97200 100644 --- a/tests/parts/scorm_api.mjs +++ b/tests/parts/scorm_api.mjs @@ -125,6 +125,7 @@ export async function with_scorm(...fns) { for(let fn of fns) { const scorm = new SCORM_API(data); window.API_1484_11 = scorm; + Numbas.storage.stores = []; const result = await fn(data, results, scorm); results.push(result);