From 8ce601847d9b8927d68f17cf545ad885ffc8ab20 Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Thu, 19 Dec 2024 10:09:54 +0000 Subject: [PATCH] student_id and initial_seed are exam-level variables The variables `student_id` and `initial_seed` are set in the exam scope and available to all questions. They can be used to coordinate the randomisation across questions. The initial seed is either a value from the built-in random number generator, or loaded from the SCORM element 'numbas.initial_seed'. The LTI provider can use this to force attempts to use a particular seed, so they match a printed copy, for example. fixes #805 --- runtime/scripts/exam.js | 18 ++- runtime/scripts/scorm-storage.js | 215 ++++++++++++++----------------- runtime/scripts/start-exam.js | 6 +- runtime/scripts/storage.js | 11 +- 4 files changed, 126 insertions(+), 124 deletions(-) diff --git a/runtime/scripts/exam.js b/runtime/scripts/exam.js index b263347bc..dd3aaeeac 100644 --- a/runtime/scripts/exam.js +++ b/runtime/scripts/exam.js @@ -636,11 +636,15 @@ Exam.prototype = /** @lends Numbas.Exam.prototype */ { init: function() { var exam = this; + if(exam.store) { + job(exam.store.init,exam.store,exam); //initialise storage + job(exam.set_exam_variables, exam); + } job(exam.chooseQuestionSubset,exam); //choose questions to use job(exam.makeQuestionList,exam); //create question objects exam.signals.on('question list initialised', function() { if(exam.store) { - job(exam.store.init,exam.store,exam); //initialise storage + job(exam.store.init_questions,exam.store,exam); //initialise question storage job(exam.store.save,exam.store); //make sure data get saved to LMS } }); @@ -671,9 +675,10 @@ Exam.prototype = /** @lends Numbas.Exam.prototype */ { } this.loading = true; var suspendData = this.store.load(this); //get saved info from storage + exam.seed = suspendData.randomSeed || exam.seed; + job(exam.set_exam_variables, exam); job(function() { var e = this; - e.seed = suspendData.randomSeed || e.seed; var numQuestions = 0; if(suspendData.questionGroupOrder) { this.questionGroupOrder = suspendData.questionGroupOrder.slice(); @@ -714,6 +719,15 @@ Exam.prototype = /** @lends Numbas.Exam.prototype */ { exam.signals.trigger('ready'); }); }, + + /** Set exam-level variables. + * + */ + set_exam_variables: function() { + this.scope.setVariable('initial_seed', Numbas.jme.wrapValue(this.seed)); + this.scope.setVariable('student_id', Numbas.jme.wrapValue(this.student_id)); + }, + /** Decide which questions to use and in what order. * * @fires Numbas.Exam#chooseQuestionSubset diff --git a/runtime/scripts/scorm-storage.js b/runtime/scripts/scorm-storage.js index 1eeacb852..74a720d10 100644 --- a/runtime/scripts/scorm-storage.js +++ b/runtime/scripts/scorm-storage.js @@ -45,19 +45,19 @@ var SCORMStorage = function() { this.getEntry(); //get all question-objective indices this.questionIndices = {}; - var numObjectives = parseInt(this.get('objectives._count'),10); + var numObjectives = parseInt(this.get('cmi.objectives._count'),10); for(var i=0;i 0 ? exam.score/exam.mark : 0) || 0); + this.set('cmi.score.raw',exam.score); + this.set('cmi.score.scaled',(exam.mark > 0 ? exam.score/exam.mark : 0) || 0); }, /** Save details about a question - save score and success status. * * @param {Numbas.Question} question */ - saveQuestion: function(question) - { + saveQuestion: function(question) { if(question.exam.loading) return; var id = this.getQuestionId(question); if(!(id in this.questionIndices)) return; var index = this.questionIndices[id]; - var prepath = 'objectives.'+index+'.'; + var prepath = 'cmi.objectives.'+index+'.'; this.set(prepath+'score.raw',question.score); this.set(prepath+'score.scaled',(question.marks > 0 ? question.score/question.marks : 0) || 0); this.set(prepath+'success_status', question.score==question.marks ? 'passed' : 'failed' ); @@ -664,24 +646,21 @@ SCORMStorage.prototype = /** @lends Numbas.storage.SCORMStorage.prototype */ { * * @param {Numbas.Question} question */ - questionSubmitted: function(question) - { + questionSubmitted: function(question) { this.save(); }, /** Record that the student displayed question advice. * * @param {Numbas.Question} question */ - adviceDisplayed: function(question) - { + adviceDisplayed: function(question) { this.setSuspendData(); }, /** Record that the student revealed the answers to a question. * * @param {Numbas.Question} question */ - answerRevealed: function(question) - { + answerRevealed: function(question) { this.setSuspendData(); this.save(); }, @@ -689,8 +668,7 @@ SCORMStorage.prototype = /** @lends Numbas.storage.SCORMStorage.prototype */ { * * @param {Numbas.parts.Part} part */ - stepsShown: function(part) - { + stepsShown: function(part) { this.setSuspendData(); this.save(); }, @@ -698,8 +676,7 @@ SCORMStorage.prototype = /** @lends Numbas.storage.SCORMStorage.prototype */ { * * @param {Numbas.parts.Part} part */ - stepsHidden: function(part) - { + stepsHidden: function(part) { this.setSuspendData(); this.save(); } diff --git a/runtime/scripts/start-exam.js b/runtime/scripts/start-exam.js index 907029bad..843cc57e5 100644 --- a/runtime/scripts/start-exam.js +++ b/runtime/scripts/start-exam.js @@ -47,7 +47,6 @@ Numbas.queueScript('start-exam',['base','util', 'exam','settings'],function() { for(var x in Numbas.extensions) { Numbas.activateExtension(x); } - var seed = Math.seedrandom(new Date().getTime()); var job = Numbas.schedule.add; job(Numbas.xml.loadXMLDocs); job(Numbas.diagnostic.load_scripts); @@ -56,9 +55,12 @@ Numbas.queueScript('start-exam',['base','util', 'exam','settings'],function() { var store = Numbas.store; var scorm_store = new Numbas.storage.scorm.SCORMStorage(); Numbas.storage.addStorage(scorm_store); + var external_seed = scorm_store.get_initial_seed(); + var seed = external_seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(); + Math.seedrandom(seed); var xml = Numbas.xml.examXML.selectSingleNode('/exam'); var exam = Numbas.exam = Numbas.createExamFromXML(xml,store,true); - exam.seed = Numbas.util.hashCode(seed); + exam.seed = seed; var entry = store.getEntry(); if(store.getMode() == 'review') { entry = 'review'; diff --git a/runtime/scripts/storage.js b/runtime/scripts/storage.js index f06eb8801..a8387ef4e 100644 --- a/runtime/scripts/storage.js +++ b/runtime/scripts/storage.js @@ -66,6 +66,9 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p init: function(exam) { this.exam = exam; }, + + init_questions: function() { + }, /** Initialise a question. * * @param {Numbas.Question} q @@ -105,6 +108,12 @@ Numbas.storage.BlankStorage.prototype = /** @lends Numbas.storage.BlankStorage.p * @abstract */ get_student_name: function() {}, + /** Get the initial seed value. + * + * @abstract + * @returns {string} + */ + get_initial_seed: function() {}, /** * Get suspended info for a question. * @@ -316,7 +325,7 @@ 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.seed, student_name: exam.student_name, score: exam.score, max_score: exam.mark,