Skip to content

Commit aa289c8

Browse files
committed
Bug fix: if a combinator template grader returned a JSON result in which the resulttable attribute was not a list of lists, a Moodle exception was thrown (Exception - count(): Argument #1 ($value) must be of type Countable|array, stdClass given).
1 parent c943f24 commit aa289c8

File tree

5 files changed

+69
-13
lines changed

5 files changed

+69
-13
lines changed

classes/combinator_grader_outcome.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public function set_mark_and_feedback($markfraction, $feedback) {
187187
}
188188
}
189189

190-
if ($this->valid_table_formats($testresults, $this->columnformats)) {
190+
if ($testresults && $this->valid_table($testresults) && $this->valid_table_formats($testresults, $this->columnformats)) {
191191
$this->format_results_table($testresults, $this->columnformats, $urls);
192192
}
193193
}
@@ -438,6 +438,31 @@ public function get_grader_state() {
438438
return empty($this->graderstate) ? '' : $this->graderstate;
439439
}
440440

441+
/**
442+
* Check if the given testresults table is a list of lists.
443+
* @param array $testresults The testresults attribute from the grader.
444+
* @return bool True iff $testresults is an array of arrays. If false,
445+
* an appropriate status error message is set.
446+
*/
447+
private function valid_table($testresults) {
448+
$ok = is_array($testresults);
449+
if ($ok) {
450+
foreach ($testresults as $row) {
451+
if (!is_array($row)) {
452+
$ok = false;
453+
}
454+
}
455+
}
456+
if (!$ok) {
457+
$error = get_string(
458+
'badresultstable',
459+
'qtype_coderunner'
460+
);
461+
$this->set_status(self::STATUS_BAD_COMBINATOR, $error);
462+
}
463+
return $ok;
464+
}
465+
441466

442467
/**
443468
* Check if the given values of column formats and test results are

edit_coderunner_form.php

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,11 +1284,6 @@ public function validation($data, $files) {
12841284
}
12851285
}
12861286

1287-
$penaltyregimeerror = $this->validate_penalty_regime($data);
1288-
if ($penaltyregimeerror) {
1289-
$errors['markinggroup'] = $penaltyregimeerror;
1290-
}
1291-
12921287
$resultcolumnsjson = trim($data['resultcolumns'] ?? '');
12931288
if ($resultcolumnsjson !== '') {
12941289
$resultcolumns = json_decode($resultcolumnsjson);
@@ -1324,6 +1319,15 @@ public function validation($data, $files) {
13241319
$errors = $this->validate_twigables();
13251320
}
13261321

1322+
if (count($errors) == 0) {
1323+
// Penalty regime is Twiggable, so we have postponed validating
1324+
// until after validate_twigables.
1325+
$penaltyregimeerror = $this->validate_penalty_regime($data);
1326+
if ($penaltyregimeerror) {
1327+
$errors['markinggroup'] = $penaltyregimeerror;
1328+
}
1329+
}
1330+
13271331
if (count($errors) == 0 && !empty($data['validateonsave'])) {
13281332
$testresult = $this->validate_sample_answer($data);
13291333
if ($testresult) {
@@ -1461,9 +1465,16 @@ private function validate_penalty_regime($data) {
14611465
$errorstring = '';
14621466
$expectedpr = '/[0-9]+(\.[0-9]*)?%?([, ] *[0-9]+(\.[0-9]*)?%?)*([, ] *...)?/';
14631467
$penaltyregime = trim($data['penaltyregime'] ?? '');
1464-
if (preg_match('/{{.*}}/', $penaltyregime)) {
1465-
return ''; // Looks like it's a Twig expression. Hope for the best.
1468+
1469+
// If twigall on, we should Twig expand the penalty regime
1470+
// before attempting to validate it.
1471+
if ($penaltyregime && $data['twigall']) {
1472+
$question = $this->formquestion;
1473+
$jsonparams = $question->templateparamsevald;
1474+
$parameters = json_decode($jsonparams, true);
1475+
$penaltyregime = $this->twig_render($penaltyregime, $parameters, true);
14661476
}
1477+
14671478
if ($penaltyregime == '') {
14681479
$errorstring = get_string('emptypenaltyregime', 'qtype_coderunner');
14691480
} else if (!preg_match($expectedpr, $penaltyregime)) {
@@ -1505,10 +1516,11 @@ private function validate_twigables() {
15051516
$question = $this->formquestion;
15061517
$jsonparams = $question->templateparamsevald;
15071518
$parameters = json_decode($jsonparams, true);
1508-
$parameters['QUESTION'] = $question;
1519+
$parameters['QUESTION'] = $question; // Shouldn't really include this but removing it might break existing questions.
15091520

15101521
// Try twig expanding everything (see question::twig_all), with strict_variables true.
1511-
foreach (['questiontext', 'answer', 'answerpreload', 'globalextra', 'prototypeextra', 'penaltyregime'] as $field) {
1522+
$twigablefields = qtype_coderunner::twigablefields();
1523+
foreach ($twigablefields as $field) {
15121524
$text = $question->$field;
15131525
if (is_array($text)) {
15141526
$text = $text['text'];

lang/en/qtype_coderunner.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
$string['badquestion'] = 'Error in question';
8888
$string['badrandomintarg'] = 'Bad argument to JSON @randomint function';
8989
$string['badrandompickarg'] = 'Bad argument to JSON @randompic function';
90+
$string['badresultstable'] = 'Bad test results table. Should be a list of lists.';
9091
$string['badsandboxparams'] = '\'Other\' field (sandbox params) must be either blank or a valid JSON record';
9192
$string['badtemplateparams'] = 'Template parameters must evaluate to blank or a valid JSON record. Got: <pre class="templateparamserror">{$a}</pre>';
9293
$string['baduiparams'] = 'UI parameters must be blank or a valid JSON record.';

question.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,10 +1020,9 @@ public function example_testcases() {
10201020
private function twig_all() {
10211021
// Twig expand everything in a context that includes the template
10221022
// parameters and the STUDENT and QUESTION objects.
1023-
$twigables = ['questiontext', 'generalfeedback', 'answer', 'answerpreload',
1024-
'globalextra', 'prototypeextra', 'penaltyregime', 'uiparameters'];
1023+
$twigables = qtype_coderunner::twigablefields();
10251024
foreach ($twigables as $twigable) {
1026-
if ($empty($this->$twigable)) {
1025+
if (!empty($this->$twigable)) {
10271026
$this->$twigable = $this->twig_expand($this->$twigable);
10281027
}
10291028
}

questiontype.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,25 @@ public static function noninherited_fields() {
173173
];
174174
}
175175

176+
/** A list of all fields in a question that are to be Twig expanded
177+
* when TwigAll is set. It excludes the template itself, which is
178+
* Twig expanded only when the question is run (as it needs a
179+
* student answer).
180+
* @return array A list of all fields to be Twig-expanded prior to rendering.
181+
*/
182+
public static function twigablefields() {
183+
return [
184+
'questiontext',
185+
'generalfeedback',
186+
'answer',
187+
'answerpreload',
188+
'globalextra',
189+
'prototypeextra',
190+
'penaltyregime',
191+
'uiparameters',
192+
];
193+
}
194+
176195
public function response_file_areas() {
177196
return ['attachments'];
178197
}

0 commit comments

Comments
 (0)