Skip to content

Commit

Permalink
the question preamble can return a promise, which will be awaited
Browse files Browse the repository at this point in the history
Prompted by someone wanting to get data from an API to use in variable
generation.

If the question's JS preamble returns a Promise, then the `preambleRun`
signal isn't fired until that promise resolves. The constants, functions
and rulesets jobs now don't happen until the promise resolves, so you
can do things like set variables in the question scope before the
variable generation happens.
  • Loading branch information
christianp committed Jan 30, 2024
1 parent c387528 commit ee9c7b6
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 30 deletions.
59 changes: 45 additions & 14 deletions runtime/scripts/question.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var createQuestionFromXML = Numbas.createQuestionFromXML = function(xml, number,
q.loadFromXML(xml);
q.finaliseLoad();
} catch(e) {
throw(new Numbas.Error('question.error creating question',{number: number, message: e.message}));
throw(new Numbas.Error('question.error creating question',{number: number+1, message: e.message}));
}
return q;
}
Expand All @@ -54,7 +54,7 @@ var createQuestionFromJSON = Numbas.createQuestionFromJSON = function(data, numb
q.loadFromJSON(data);
q.finaliseLoad();
} catch(e) {
throw(new Numbas.Error('question.error creating question',{number: number, message: e.message},e));
throw(new Numbas.Error('question.error creating question',{number: number+1, message: e.message},e));
}
return q;
}
Expand Down Expand Up @@ -234,6 +234,28 @@ Question.prototype = /** @lends Numbas.Question.prototype */
*/
store: undefined,

/** Throw an error, with the question's identifier prepended to the message.
*
* @param {string} message
* @param {object} args - Arguments for the error message.
* @param {Error} [originalError] - If this is a re-thrown error, the original error object.
* @fires Numbas.Question#event:error
* @throws {Numbas.Error}
*/
error: function(message, args, originalError) {
if(originalError && originalError.originalMessages && originalError.originalMessages[0]=='question.error') {
throw(originalError);
}
var nmessage = R.apply(this, [message, args]);
if(nmessage != message) {
originalError = new Error(nmessage);
originalError.originalMessages = [message].concat(originalError.originalMessages || []);
}
var niceName = this.name;
this.events.trigger('error', message, args, originalError);
throw(new Numbas.Error('question.error',{number: this.number+1, message: nmessage},originalError));
},

/** Load the question's settings from an XML <question> node.
*
* @param {Element} xml
Expand Down Expand Up @@ -756,7 +778,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
}
}
});
q.signals.on('constantsLoaded', function() {
q.signals.on(['preambleRun', 'constantsLoaded'], function() {
var defined_constants = Numbas.jme.variables.makeConstants(q.constantsTodo.custom,q.scope);
q.constantsTodo.builtin.forEach(function(c) {
if(!c.enable) {
Expand All @@ -769,12 +791,12 @@ Question.prototype = /** @lends Numbas.Question.prototype */
});
q.signals.trigger('constantsMade');
});
q.signals.on('functionsLoaded', function() {
q.signals.on(['preambleRun', 'functionsLoaded'], function() {
var functions = Numbas.jme.variables.makeFunctions(q.functionsTodo,q.scope,{question:q});
q.scope = new jme.Scope([q.scope,{functions: functions}]);
q.signals.trigger('functionsMade');
});
q.signals.on('rulesetsLoaded',function() {
q.signals.on(['preambleRun', 'rulesetsLoaded'],function() {
Numbas.jme.variables.makeRulesets(q.rulesets,q.scope);
q.signals.trigger('rulesetsMade');
});
Expand All @@ -786,7 +808,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
var names = jme.variables.splitVariableNames(name);
names.forEach(function(n) {
if(seen_names[n]) {
throw(new Numbas.Error("jme.variables.duplicate definition",{name:n}));
q.error("jme.variables.duplicate definition",{name:n});
}
seen_names[n] = true;
});
Expand All @@ -795,15 +817,15 @@ Question.prototype = /** @lends Numbas.Question.prototype */
if(definition=='') {
return;
}
throw(new Numbas.Error('jme.variables.empty name'));
q.error('jme.variables.empty name');
}
if(definition=='') {
throw(new Numbas.Error('jme.variables.empty definition',{name:name}));
q.error('jme.variables.empty definition',{name:name});
}
try {
var tree = Numbas.jme.compile(definition);
} catch(e) {
throw(new Numbas.Error('variable.error in variable definition',{name:name}));
q.error('variable.error in variable definition',{name:name});
}
var vars = Numbas.jme.findvars(tree,[],q.scope);
todo[name] = {
Expand All @@ -830,7 +852,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
conditionSatisfied = result.conditionSatisfied;
}
if(!conditionSatisfied) {
throw(new Numbas.Error('jme.variables.question took too many runs to generate variables'));
q.error('jme.variables.question took too many runs to generate variables');
} else {
q.scope = scope;
}
Expand Down Expand Up @@ -1092,13 +1114,22 @@ Question.prototype = /** @lends Numbas.Question.prototype */
*/
runPreamble: function() {
var jfn = new Function(['question'], this.preamble.js);
var res;
try {
jfn(this);
res = jfn(this);
return Promise.resolve(res).then(() => {
this.signals.trigger('preambleRun');
}).catch(e => {
try {
this.error('question.preamble.error',{message: e.message});
} catch(e) {
Numbas.schedule.halt(e);
}
});
} catch(e) {
var errorName = e.name=='SyntaxError' ? 'question.preamble.syntax error' : 'question.preamble.error';
throw(new Numbas.Error(errorName,{'number':this.number+1,message:e.message}));
this.error(errorName,{message: e.message});
}
this.signals.trigger('preambleRun');
},
/** Get the part object corresponding to a path.
*
Expand All @@ -1109,7 +1140,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
{
var p = this.partDictionary[path];
if(!p) {
throw(new Numbas.Error("question.no such part",{path:path}));
this.error("question.no such part",{path:path});
}
return p;
},
Expand Down
59 changes: 45 additions & 14 deletions tests/numbas-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -22054,7 +22054,7 @@ var createQuestionFromXML = Numbas.createQuestionFromXML = function(xml, number,
q.loadFromXML(xml);
q.finaliseLoad();
} catch(e) {
throw(new Numbas.Error('question.error creating question',{number: number, message: e.message}));
throw(new Numbas.Error('question.error creating question',{number: number+1, message: e.message}));
}
return q;
}
Expand All @@ -22075,7 +22075,7 @@ var createQuestionFromJSON = Numbas.createQuestionFromJSON = function(data, numb
q.loadFromJSON(data);
q.finaliseLoad();
} catch(e) {
throw(new Numbas.Error('question.error creating question',{number: number, message: e.message},e));
throw(new Numbas.Error('question.error creating question',{number: number+1, message: e.message},e));
}
return q;
}
Expand Down Expand Up @@ -22255,6 +22255,28 @@ Question.prototype = /** @lends Numbas.Question.prototype */
*/
store: undefined,

/** Throw an error, with the question's identifier prepended to the message.
*
* @param {string} message
* @param {object} args - Arguments for the error message.
* @param {Error} [originalError] - If this is a re-thrown error, the original error object.
* @fires Numbas.Question#event:error
* @throws {Numbas.Error}
*/
error: function(message, args, originalError) {
if(originalError && originalError.originalMessages && originalError.originalMessages[0]=='question.error') {
throw(originalError);
}
var nmessage = R.apply(this, [message, args]);
if(nmessage != message) {
originalError = new Error(nmessage);
originalError.originalMessages = [message].concat(originalError.originalMessages || []);
}
var niceName = this.name;
this.events.trigger('error', message, args, originalError);
throw(new Numbas.Error('question.error',{number: this.number+1, message: nmessage},originalError));
},

/** Load the question's settings from an XML <question> node.
*
* @param {Element} xml
Expand Down Expand Up @@ -22777,7 +22799,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
}
}
});
q.signals.on('constantsLoaded', function() {
q.signals.on(['preambleRun', 'constantsLoaded'], function() {
var defined_constants = Numbas.jme.variables.makeConstants(q.constantsTodo.custom,q.scope);
q.constantsTodo.builtin.forEach(function(c) {
if(!c.enable) {
Expand All @@ -22790,12 +22812,12 @@ Question.prototype = /** @lends Numbas.Question.prototype */
});
q.signals.trigger('constantsMade');
});
q.signals.on('functionsLoaded', function() {
q.signals.on(['preambleRun', 'functionsLoaded'], function() {
var functions = Numbas.jme.variables.makeFunctions(q.functionsTodo,q.scope,{question:q});
q.scope = new jme.Scope([q.scope,{functions: functions}]);
q.signals.trigger('functionsMade');
});
q.signals.on('rulesetsLoaded',function() {
q.signals.on(['preambleRun', 'rulesetsLoaded'],function() {
Numbas.jme.variables.makeRulesets(q.rulesets,q.scope);
q.signals.trigger('rulesetsMade');
});
Expand All @@ -22807,7 +22829,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
var names = jme.variables.splitVariableNames(name);
names.forEach(function(n) {
if(seen_names[n]) {
throw(new Numbas.Error("jme.variables.duplicate definition",{name:n}));
q.error("jme.variables.duplicate definition",{name:n});
}
seen_names[n] = true;
});
Expand All @@ -22816,15 +22838,15 @@ Question.prototype = /** @lends Numbas.Question.prototype */
if(definition=='') {
return;
}
throw(new Numbas.Error('jme.variables.empty name'));
q.error('jme.variables.empty name');
}
if(definition=='') {
throw(new Numbas.Error('jme.variables.empty definition',{name:name}));
q.error('jme.variables.empty definition',{name:name});
}
try {
var tree = Numbas.jme.compile(definition);
} catch(e) {
throw(new Numbas.Error('variable.error in variable definition',{name:name}));
q.error('variable.error in variable definition',{name:name});
}
var vars = Numbas.jme.findvars(tree,[],q.scope);
todo[name] = {
Expand All @@ -22851,7 +22873,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
conditionSatisfied = result.conditionSatisfied;
}
if(!conditionSatisfied) {
throw(new Numbas.Error('jme.variables.question took too many runs to generate variables'));
q.error('jme.variables.question took too many runs to generate variables');
} else {
q.scope = scope;
}
Expand Down Expand Up @@ -23113,13 +23135,22 @@ Question.prototype = /** @lends Numbas.Question.prototype */
*/
runPreamble: function() {
var jfn = new Function(['question'], this.preamble.js);
var res;
try {
jfn(this);
res = jfn(this);
return Promise.resolve(res).then(() => {
this.signals.trigger('preambleRun');
}).catch(e => {
try {
this.error('question.preamble.error',{message: e.message});
} catch(e) {
Numbas.schedule.halt(e);
}
});
} catch(e) {
var errorName = e.name=='SyntaxError' ? 'question.preamble.syntax error' : 'question.preamble.error';
throw(new Numbas.Error(errorName,{'number':this.number+1,message:e.message}));
this.error(errorName,{message: e.message});
}
this.signals.trigger('preambleRun');
},
/** Get the part object corresponding to a path.
*
Expand All @@ -23130,7 +23161,7 @@ Question.prototype = /** @lends Numbas.Question.prototype */
{
var p = this.partDictionary[path];
if(!p) {
throw(new Numbas.Error("question.no such part",{path:path}));
this.error("question.no such part",{path:path});
}
return p;
},
Expand Down
4 changes: 2 additions & 2 deletions tests/parts/part-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Numbas.queueScript('part_tests',['qunit','json','jme','localisation','parts/numb
function question_test(name,data,test_fn,error_fn) {
QUnit.test(name, async function(assert) {
var done = assert.async();
var q = Numbas.createQuestionFromJSON(data);
var q = Numbas.createQuestionFromJSON(data, 0);
q.generateVariables();
q.signals.on('ready').then(function() {
test_fn(assert,q);
Expand Down Expand Up @@ -825,7 +825,7 @@ Numbas.queueScript('part_tests',['qunit','json','jme','localisation','parts/numb
var p0 = q.getPart('p0');
p0.storeAnswer('4');
await submit_part(p0);
assert.equal(p0.markingFeedback[0].message, "There was an error in the adaptive marking for this part. Please report this. Can't find part p1.");
assert.equal(p0.markingFeedback[0].message, "There was an error in the adaptive marking for this part. Please report this. Question 1: Can't find part p1.");
done();
}
)
Expand Down

0 comments on commit ee9c7b6

Please sign in to comment.