From 0d9f92920dc822ce1a51ad9001ef08694fe65629 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 9 Feb 2026 14:30:18 +0000 Subject: [PATCH 1/7] iss1678 - Default inputs/PRTs changes first pass. --- api/util/StackQuestionLoader.php | 32 +++++++++++++++++++------------- questiontype.php | 32 +++++++++++++++++++------------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index f20491cc2e9..22def334031 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -296,10 +296,13 @@ public static function loadxml($xml, $includetests = false) { } if (empty($inputmap) && $question->defaultmark) { - $defaultinput = new \SimpleXMLElement(''); - $defaultinput->addChild('name', self::get_default('input', 'name', 'ans1')); - $defaultinput->addChild('tans', self::get_default('input', 'tans', 'ta1')); - $inputmap[self::get_default('input', 'name', 'ans1')] = $defaultinput; + preg_match_all("/\[\[input:([^\]]*)\]\]/", $question->questiontext, $namedinputs); + foreach ($namedinputs as $namedinput) { + $defaultinput = new \SimpleXMLElement(''); + $defaultinput->addChild('name', $namedinput); + $defaultinput->addChild('tans', self::get_default('input', 'tans', 'ta1')); + $inputmap[$namedinput] = $defaultinput; + } } $requiredparams = \stack_input_factory::get_parameters_used(); @@ -373,15 +376,18 @@ public static function loadxml($xml, $includetests = false) { } if (empty($prtmap) && $question->defaultmark) { - $defaultprt = new \SimpleXMLElement(''); - $defaultprt->addChild('name', self::get_default('prt', 'name', 'prt1')); - $defaultnode = $defaultprt->addChild('node'); - $defaultnode->addChild('name', self::get_default('node', 'name', '0')); - $defaultnode->addChild('sans', self::get_default('node', 'sans', 'ans1')); - $defaultnode->addChild('tans', self::get_default('node', 'tans', 'ta1')); - $defaultnode->addChild('trueanswernote', self::get_default('node', 'trueanswernote', 'prt1-1-T')); - $defaultnode->addChild('falseanswernote', self::get_default('node', 'falseanswernote', 'prt1-1-F')); - $prtmap[self::get_default('prt', 'name', 'prt1')] = $defaultprt; + preg_match_all("/\[\[prt:([^\]]*)\]\]/", $question->questiontext . $question->specificfeedback, $namedprts); + foreach ($namedprts as $namedprt) { + $defaultprt = new \SimpleXMLElement(''); + $defaultprt->addChild('name', $namedprt); + $defaultnode = $defaultprt->addChild('node'); + $defaultnode->addChild('name', self::get_default('node', 'name', '0')); + $defaultnode->addChild('sans', self::get_default('node', 'sans', 'ans1')); + $defaultnode->addChild('tans', self::get_default('node', 'tans', 'ta1')); + $defaultnode->addChild('trueanswernote', self::get_default('node', 'trueanswernote', 'prt1-1-T')); + $defaultnode->addChild('falseanswernote', self::get_default('node', 'falseanswernote', 'prt1-1-F')); + $prtmap[$namedprt] = $defaultprt; + } } foreach ($prtmap as $prtdata) { diff --git a/questiontype.php b/questiontype.php index 6bd3531cb1a..0fdca5a283f 100644 --- a/questiontype.php +++ b/questiontype.php @@ -1876,9 +1876,12 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = } } else { if ($fromform->defaultmark) { - $defaultinput = []; - $defaultinput['#'] = ['name' => [0 => ['#' => 'ans1']], 'tans' => [0 => ['#' => 'ta1']]]; - $this->import_xml_input($defaultinput, $fromform, $format); + preg_match_all("/\[\[input:([^\]]*)\]\]/", $fromform->questiontext, $namedinputs); + foreach ($namedinputs as $namedinput) { + $defaultinput = []; + $defaultinput['#'] = ['name' => [0 => ['#' => $namedinput]], 'tans' => [0 => ['#' => 'ta1']]]; + $this->import_xml_input($defaultinput, $fromform, $format); + } } } @@ -1888,16 +1891,19 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = } } else { if ($fromform->defaultmark) { - $defaultnode = [ - 'name' => [0 => ['#' => 0]], - 'sans' => [0 => ['#' => 'ans1']], - 'tans' => [0 => ['#' => 'ta1']], - 'trueanswernote' => [0 => ['#' => 'prt1-1-T']], - 'falseanswernote' => [0 => ['#' => 'prt1-1-F']], - ]; - $defaultprt = []; - $defaultprt['#'] = ['name' => [0 => ['#' => 'prt1']], 'node' => [['#' => $defaultnode]]]; - $this->import_xml_prt($defaultprt, $fromform, $format); + preg_match_all("/\[\[prt:([^\]]*)\]\]/", $fromform->questiontext . $fromform->specificfeedback, $namedprts); + foreach ($namedprts as $namedprt) { + $defaultnode = [ + 'name' => [0 => ['#' => 0]], + 'sans' => [0 => ['#' => 'ans1']], + 'tans' => [0 => ['#' => 'ta1']], + 'trueanswernote' => [0 => ['#' => 'prt1-1-T']], + 'falseanswernote' => [0 => ['#' => 'prt1-1-F']], + ]; + $defaultprt = []; + $defaultprt['#'] = ['name' => [0 => ['#' => $namedprt]], 'node' => [['#' => $defaultnode]]]; + $this->import_xml_prt($defaultprt, $fromform, $format); + } } } From 4a82d0f87caefe96e1476dc87ad09c0d5fd5d716 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 10 Feb 2026 14:16:26 +0000 Subject: [PATCH 2/7] iss1678 - Simpify searching --- api/util/StackQuestionLoader.php | 23 +++++++++++-------- questiontype.php | 39 +++++++++++++++----------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index 22def334031..13f9938c575 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -295,13 +295,16 @@ public static function loadxml($xml, $includetests = false) { $inputmap[(string) $input->name] = $input; } - if (empty($inputmap) && $question->defaultmark) { - preg_match_all("/\[\[input:([^\]]*)\]\]/", $question->questiontext, $namedinputs); - foreach ($namedinputs as $namedinput) { + if (empty($inputmap)) { + $inputname = self::get_default('input', 'name', 'ans1'); + if (preg_match("/\[\[input:{$inputname}\]\]/", $question->questiontext)) { $defaultinput = new \SimpleXMLElement(''); - $defaultinput->addChild('name', $namedinput); + $defaultinput->addChild('name', $inputname); $defaultinput->addChild('tans', self::get_default('input', 'tans', 'ta1')); - $inputmap[$namedinput] = $defaultinput; + $inputmap[$inputname] = $defaultinput; + } else { + // We've not got any inputs. Set default mark to 0. + $question->defaultmark = 0; } } @@ -375,18 +378,18 @@ public static function loadxml($xml, $includetests = false) { $prtmap[(string) $prt->name] = $prt; } - if (empty($prtmap) && $question->defaultmark) { - preg_match_all("/\[\[prt:([^\]]*)\]\]/", $question->questiontext . $question->specificfeedback, $namedprts); - foreach ($namedprts as $namedprt) { + if (empty($prtmap)) { + $prtname = self::get_default('prt', 'name', 'prt1'); + if (preg_match("/\[\[feedback:{$prtname}\]\]/", $question->questiontext . $question->specificfeedback)) { $defaultprt = new \SimpleXMLElement(''); - $defaultprt->addChild('name', $namedprt); + $defaultprt->addChild('name', $prtname); $defaultnode = $defaultprt->addChild('node'); $defaultnode->addChild('name', self::get_default('node', 'name', '0')); $defaultnode->addChild('sans', self::get_default('node', 'sans', 'ans1')); $defaultnode->addChild('tans', self::get_default('node', 'tans', 'ta1')); $defaultnode->addChild('trueanswernote', self::get_default('node', 'trueanswernote', 'prt1-1-T')); $defaultnode->addChild('falseanswernote', self::get_default('node', 'falseanswernote', 'prt1-1-F')); - $prtmap[$namedprt] = $defaultprt; + $prtmap[$prtname] = $defaultprt; } } diff --git a/questiontype.php b/questiontype.php index 0fdca5a283f..23a8c10723c 100644 --- a/questiontype.php +++ b/questiontype.php @@ -1875,13 +1875,13 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = $this->import_xml_input($inputxml, $fromform, $format); } } else { - if ($fromform->defaultmark) { - preg_match_all("/\[\[input:([^\]]*)\]\]/", $fromform->questiontext, $namedinputs); - foreach ($namedinputs as $namedinput) { - $defaultinput = []; - $defaultinput['#'] = ['name' => [0 => ['#' => $namedinput]], 'tans' => [0 => ['#' => 'ta1']]]; - $this->import_xml_input($defaultinput, $fromform, $format); - } + if (preg_match("/\[\[input:ans1\]\]/", $fromform->questiontext)) { + $defaultinput = []; + $defaultinput['#'] = ['name' => [0 => ['#' => 'ans1']], 'tans' => [0 => ['#' => 'ta1']]]; + $this->import_xml_input($defaultinput, $fromform, $format); + } else { + // We've not got any inputs. Set default mark to 0. + $fromform->defaultmark = 0; } } @@ -1890,20 +1890,17 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = $structurerepairs .= $this->import_xml_prt($prtxml, $fromform, $format); } } else { - if ($fromform->defaultmark) { - preg_match_all("/\[\[prt:([^\]]*)\]\]/", $fromform->questiontext . $fromform->specificfeedback, $namedprts); - foreach ($namedprts as $namedprt) { - $defaultnode = [ - 'name' => [0 => ['#' => 0]], - 'sans' => [0 => ['#' => 'ans1']], - 'tans' => [0 => ['#' => 'ta1']], - 'trueanswernote' => [0 => ['#' => 'prt1-1-T']], - 'falseanswernote' => [0 => ['#' => 'prt1-1-F']], - ]; - $defaultprt = []; - $defaultprt['#'] = ['name' => [0 => ['#' => $namedprt]], 'node' => [['#' => $defaultnode]]]; - $this->import_xml_prt($defaultprt, $fromform, $format); - } + if (preg_match("/\[\[feedback:prt1\]\]/", $fromform->questiontext . $fromform->specificfeedback['text'])) { + $defaultnode = [ + 'name' => [0 => ['#' => 0]], + 'sans' => [0 => ['#' => 'ans1']], + 'tans' => [0 => ['#' => 'ta1']], + 'trueanswernote' => [0 => ['#' => 'prt1-1-T']], + 'falseanswernote' => [0 => ['#' => 'prt1-1-F']], + ]; + $defaultprt = []; + $defaultprt['#'] = ['name' => [0 => ['#' => 'prt1']], 'node' => [['#' => $defaultnode]]]; + $this->import_xml_prt($defaultprt, $fromform, $format); } } From 6a3fa801443b6b672ef6d0c6e21f9c41e8fbe700 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 11 Feb 2026 09:47:36 +0000 Subject: [PATCH 3/7] iss1678 - Fix default PRT if no input. --- api/util/StackQuestionLoader.php | 14 ++++++++++---- questiontype.php | 12 +++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index 13f9938c575..a95994d8bcb 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -138,10 +138,16 @@ public static function loadxml($xml, $includetests = false) { $question->questionnoteformat = isset($xmldata->question->questionnote['format']) ? (string) $xmldata->question->questionnote['format'] : self::get_default('question', 'questionnoteformat', 'html'); - $question->specificfeedback = - isset($xmldata->question->specificfeedback->text) ? - (string) $xmldata->question->specificfeedback->text : - self::get_default('question', 'specificfeedback', '[[feedback:prt1]]'); + if (isset($xmldata->question->specificfeedback->text)) { + $question->specificfeedback = (string) $xmldata->question->specificfeedback->text; + } else { + $inputname = self::get_default('input', 'name', 'ans1'); + if (preg_match("/\[\[input:{$inputname}\]\]/", $question->questiontext)) { + $question->specificfeedback = self::get_default('question', 'specificfeedback', '[[feedback:prt1]]'); + } else { + $question->specificfeedback = ''; + } + } $question->specificfeedbackformat = isset($xmldata->question->specificfeedback['format']) ? (string) $xmldata->question->specificfeedback['format'] : diff --git a/questiontype.php b/questiontype.php index 23a8c10723c..57b1d865b8f 100644 --- a/questiontype.php +++ b/questiontype.php @@ -1808,7 +1808,17 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = if (isset($fromform->specificfeedbackformat)) { $fformat = $fromform->specificfeedbackformat; } - $fromform->specificfeedback = $this->import_xml_text($xml, 'specificfeedback', $format, $fformat, '[[feedback:prt1]]'); + + $fromform->specificfeedback = $this->import_xml_text($xml, 'specificfeedback', $format, $fformat, 'default_placeholder'); + // We need a temporary placeholder to differentiate user-supplied blank feedback (which we leave) from absent + // feedback (which we may need to replace). + if ($fromform->specificfeedback['text'] === 'default_placeholder') { + if (preg_match("/\[\[input:ans1\]\]/", $fromform->questiontext)) { + $fromform->specificfeedback['text'] = '[[feedback:prt1]]'; + } else { + $fromform->specificfeedback['text'] = ''; + } + } $fformat = FORMAT_HTML; if (isset($fromform->questionnoteformat)) { $fformat = $fromform->questionnoteformat; From 537c495af986f56572998d87ded4349658911eca Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 11 Feb 2026 11:42:26 +0000 Subject: [PATCH 4/7] iss1678 - Fix YML diff --- api/util/StackQuestionLoader.php | 38 ++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index a95994d8bcb..7983c6dfb39 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -141,8 +141,7 @@ public static function loadxml($xml, $includetests = false) { if (isset($xmldata->question->specificfeedback->text)) { $question->specificfeedback = (string) $xmldata->question->specificfeedback->text; } else { - $inputname = self::get_default('input', 'name', 'ans1'); - if (preg_match("/\[\[input:{$inputname}\]\]/", $question->questiontext)) { + if (preg_match("/\[\[input:{" . self::get_default('input', 'name', 'ans1') . "}\]\]/", $question->questiontext)) { $question->specificfeedback = self::get_default('question', 'specificfeedback', '[[feedback:prt1]]'); } else { $question->specificfeedback = ''; @@ -764,6 +763,25 @@ public static function detect_differences($xml) { } $plaindata = self::xml_to_array($xmldata); $diff = self::obj_diff(self::$defaults['question'], $plaindata['question']); + + $isquestiontext = isset($plaindata['question']['questiontext']); + $isdefaultinput = preg_match( + "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", + self::get_default('question', 'questiontext', '

Default question

[[input:ans1]] [[validation:ans1]]

') + ); + $isrequesteddefaultinput = preg_match( + "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", + $plaindata['question']['questiontext'] + ); + $isfeedback = isset($plaindata['question']['specificfeedback']); + $isdefaultprt = preg_match( + "/\[\[feedback:" . self::get_default('prt', 'name', 'prt1') . "\]\]/", + self::get_default('question', 'specificfeedback', '[[feedback:prt1]]') + ); + $isrequesteddefaultprt = preg_match( + "/\[\[feedback:{" . self::get_default('prt', 'name', 'prt1') . "}\]\]/", + $plaindata['question']['questiontext'] . $plaindata['question']['specificfeedback'] + ); if (!empty($plaindata['question']['input'])) { $diffinputs = []; foreach ($plaindata['question']['input'] as $input) { @@ -771,7 +789,9 @@ public static function detect_differences($xml) { $diffinputs[] = $diffinput; } $diff['input'] = $diffinputs; - } else if (!isset($plaindata['question']['defaultgrade']) || $plaindata['question']['defaultgrade']) { + // We need to create an input if questiontext contains [[input:ansnamedefault]] or + // questiontext doesn't exist and default contains [[input:ansnamedefault]]. + } else if ((!$isquestiontext && $isdefaultinput) || $isrequesteddefaultinput) { $diff['input'] = [['name' => self::get_default('input', 'name', 'ans1'), 'type' => self::get_default('input', 'type', 'algebraic'), 'tans' => self::get_default('input', 'tans', 'ta1'), @@ -782,6 +802,11 @@ public static function detect_differences($xml) { 'showvalidation' => self::get_default('input', 'showvalidation', '1')]]; } else { $diff['input'] = []; + if (self::get_default('question', 'defaultgrade', 1) !== 0) { + $diff['defaultgrade'] = '0'; + } else { + unset($diff['defaultgrade']); + } } if (!empty($plaindata['question']['prt'])) { $diffprts = []; @@ -820,7 +845,12 @@ public static function detect_differences($xml) { $diffprts[] = $diffprt; } $diff['prt'] = $diffprts; - } else if (!isset($plaindata['question']['defaultgrade']) || $plaindata['question']['defaultgrade']) { + // We need to create a PRT if questiontext contains [[input:ansnamedefault]] or + // questiontext doesn't exist and default contains [[input:ansnamedefault]]. + } else if ( + ((!$isfeedback && $isdefaultprt) || $isrequesteddefaultprt) && + ((!$isquestiontext && $isdefaultinput) || $isrequesteddefaultinput) + ) { $prtnode = ['name' => self::get_default('node', 'name', '0'), 'answertest' => self::get_default('node', 'answertest', 'AlgEquiv'), ]; if (substr($prtnode['answertest'], 0, 2) !== 'AT') { From f61777845f35275576e647e07114e7a6ce853d2c Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 13 Feb 2026 12:31:30 +0000 Subject: [PATCH 5/7] iss1678 - Unit tests --- api/questiondefaults.yml | 2 +- api/util/StackQuestionLoader.php | 19 +-- tests/api_stackquestionloader_test.php | 192 ++++++++++++++++++++--- tests/fixtures/apifixtures.class.php | 39 +++++ tests/fixtures/questiondefaultssugar.yml | 2 +- tests/questiontype_test.php | 135 ++++++++++++++++ 6 files changed, 359 insertions(+), 30 deletions(-) diff --git a/api/questiondefaults.yml b/api/questiondefaults.yml index deda08420ea..e87b062b41b 100644 --- a/api/questiondefaults.yml +++ b/api/questiondefaults.yml @@ -9,7 +9,7 @@ question: penalty: '0.1' hidden: '0' idnumber: '' - stackversion: '2025102200' + stackversion: '' questionvariables: 'ta1:1;' specificfeedback: '

[[feedback:prt1]]

' specificfeedbackformat: html diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index 7983c6dfb39..3644d9d6eb3 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -141,7 +141,7 @@ public static function loadxml($xml, $includetests = false) { if (isset($xmldata->question->specificfeedback->text)) { $question->specificfeedback = (string) $xmldata->question->specificfeedback->text; } else { - if (preg_match("/\[\[input:{" . self::get_default('input', 'name', 'ans1') . "}\]\]/", $question->questiontext)) { + if (preg_match("/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", $question->questiontext)) { $question->specificfeedback = self::get_default('question', 'specificfeedback', '[[feedback:prt1]]'); } else { $question->specificfeedback = ''; @@ -766,19 +766,20 @@ public static function detect_differences($xml) { $isquestiontext = isset($plaindata['question']['questiontext']); $isdefaultinput = preg_match( - "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", + "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", self::get_default('question', 'questiontext', '

Default question

[[input:ans1]] [[validation:ans1]]

') ); - $isrequesteddefaultinput = preg_match( - "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", + $isrequesteddefaultinput = isset($plaindata['question']['questiontext']) && preg_match( + "/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", $plaindata['question']['questiontext'] ); $isfeedback = isset($plaindata['question']['specificfeedback']); $isdefaultprt = preg_match( - "/\[\[feedback:" . self::get_default('prt', 'name', 'prt1') . "\]\]/", + "/\[\[feedback:" . self::get_default('prt', 'name', 'prt1') . "\]\]/", self::get_default('question', 'specificfeedback', '[[feedback:prt1]]') ); - $isrequesteddefaultprt = preg_match( + $isrequesteddefaultprt = isset($plaindata['question']['questiontext']) && + isset($plaindata['question']['specificfeedback']) && preg_match( "/\[\[feedback:{" . self::get_default('prt', 'name', 'prt1') . "}\]\]/", $plaindata['question']['questiontext'] . $plaindata['question']['specificfeedback'] ); @@ -848,14 +849,14 @@ public static function detect_differences($xml) { // We need to create a PRT if questiontext contains [[input:ansnamedefault]] or // questiontext doesn't exist and default contains [[input:ansnamedefault]]. } else if ( - ((!$isfeedback && $isdefaultprt) || $isrequesteddefaultprt) && + ((!$isfeedback && $isdefaultprt) || $isrequesteddefaultprt) && ((!$isquestiontext && $isdefaultinput) || $isrequesteddefaultinput) ) { $prtnode = ['name' => self::get_default('node', 'name', '0'), 'answertest' => self::get_default('node', 'answertest', 'AlgEquiv'), ]; if (substr($prtnode['answertest'], 0, 2) !== 'AT') { - $prtnode['sans'] = self::get_default('node', 'sans', 'sans'); - $prtnode['tans'] = self::get_default('node', 'tans', 'tans'); + $prtnode['sans'] = self::get_default('node', 'sans', 'ans1'); + $prtnode['tans'] = self::get_default('node', 'tans', 'ta1'); } $prtnode['quiet'] = self::get_default('node', 'quiet', '0'); $diff['prt'] = [['name' => self::get_default('prt', 'name', 'prt1'), diff --git a/tests/api_stackquestionloader_test.php b/tests/api_stackquestionloader_test.php index abdf30a9afe..89e91edf1b3 100644 --- a/tests/api_stackquestionloader_test.php +++ b/tests/api_stackquestionloader_test.php @@ -421,11 +421,12 @@ public function test_detect_difference_yml(): void { $yaml = file_get_contents(__DIR__ . '/fixtures/questionyml.yml'); $diff = StackQuestionLoader::detect_differences($yaml); $diffarray = Yaml::parse($diff); - $this->assertEquals(10, count($diffarray)); + $this->assertEquals(11, count($diffarray)); $expected = [ 'name' => 'Test question', 'questiontext' => "

Question

[[input:ans1]] [[validation:ans1]]

\n " . "

[[input:ans2]] [[validation:ans2]]

\n", + 'stackversion' => '2025042500', 'questionvariables' => 'ta1:1;ta2:2;', 'questionsimplify' => '1', 'prtcorrect' => '

Correct answer*, well done.

', @@ -521,7 +522,7 @@ public function test_detect_difference_yml(): void { ], ]; $expectedstring = "name: 'Test question'\nquestiontext: |\n

Question

[[input:ans1]] [[validation:ans1]]

" . - "\n

[[input:ans2]] [[validation:ans2]]

\nquestionvariables: 'ta1:1;ta2:2;'" . + "\n

[[input:ans2]] [[validation:ans2]]

\nstackversion: '2025042500'\nquestionvariables: 'ta1:1;ta2:2;'" . "\nquestionsimplify: '1'\nprtcorrect: '

" . " Correct answer*, well done.

'\nmultiplicationsign: cross\ninput:\n - " . "name: ans1\n type: algebraic\n tans: ta1\n boxsize: '25'\n forbidfloat: '1'\n " . @@ -546,7 +547,7 @@ public function test_detect_difference_yml(): void { StackQuestionLoader::$defaults = Yaml::parseFile(__DIR__ . '/fixtures/questiondefaultssugar.yml'); $diff = StackQuestionLoader::detect_differences($yaml); $diffarray = Yaml::parse($diff); - $this->assertEquals(10, count($diffarray)); + $this->assertEquals(11, count($diffarray)); $expected['prt'][0]['node'][0] = [ 'name' => '0', 'answertest' => 'ATAlgEquiv(ans1,ta1)', @@ -596,7 +597,7 @@ public function test_detect_difference_yml(): void { ], ], ]; - $diff = StackQuestionLoader::detect_differences($blankxml, null); + $diff = StackQuestionLoader::detect_differences($blankxml); $diffarray = Yaml::parse($diff); $this->assertEquals(4, count($diffarray)); $this->assertEqualsCanonicalizing($expected, $diffarray); @@ -615,41 +616,116 @@ public function test_detect_difference_yml(): void { $this->assertEqualsCanonicalizing($expected, $diffarray); set_config('stackapi', false, 'qtype_stack'); - // Test the difference detection with an info XML question. - $infoxml = '0'; + // Empty question with default grade. + $xml = stack_api_test_data::get_question_string('emptygrade'); $expected = [ 'name' => 'Default', + 'defaultgrade' => '2', 'questionsimplify' => '1', - 'defaultgrade' => '0', - 'input' => [], - 'prt' => [], + 'input' => [ + [ + 'name' => 'ans1', + 'type' => 'algebraic', + 'tans' => 'ta1', + 'forbidfloat' => '1', + 'requirelowestterms' => '0', + 'checkanswertype' => '0', + 'mustverify' => '1', + 'showvalidation' => '1', + ], + ], + 'prt' => [ + [ + 'name' => 'prt1', + 'autosimplify' => '1', + 'feedbackstyle' => '1', + 'node' => [ + [ + 'name' => '0', + 'answertest' => 'AlgEquiv', + 'sans' => 'ans1', + 'tans' => 'ta1', + 'quiet' => '0', + ], + ], + ], + ], ]; - $diff = StackQuestionLoader::detect_differences($infoxml, null); + $diff = StackQuestionLoader::detect_differences($xml); $diffarray = Yaml::parse($diff); $this->assertEquals(5, count($diffarray)); + $this->assertEqualsCanonicalizing($expected, $diffarray); + // Check results when using answertest summary in defaults. + set_config('stackapi', true, 'qtype_stack'); + StackQuestionLoader::$defaults = Yaml::parseFile(__DIR__ . '/fixtures/questiondefaultssugar.yml'); + $diff = StackQuestionLoader::detect_differences($xml); + $diffarray = Yaml::parse($diff); + $this->assertEquals(5, count($diffarray)); + $expected['prt'][0]['node'][0] = [ + 'name' => '0', + 'answertest' => 'ATAlgEquiv(ans1,ta1)', + 'quiet' => '0', + ]; $this->assertEqualsCanonicalizing($expected, $diffarray); - // Test the difference detection with an info XML question. - $infoxml = '0'; + // No inputs, blank specific feedback. + $xml = stack_api_test_data::get_question_string('noinputblankspecific'); $expected = [ 'name' => 'Default', + 'questiontext' => 'Question wording', 'defaultgrade' => '0', + 'specificfeedback' => '', 'questionsimplify' => '1', 'input' => [], 'prt' => [], ]; - $diff = StackQuestionLoader::detect_differences($infoxml); + $diff = StackQuestionLoader::detect_differences($xml); $diffarray = Yaml::parse($diff); - $this->assertEquals(5, count($diffarray)); + $this->assertEquals(7, count($diffarray)); $this->assertEqualsCanonicalizing($expected, $diffarray); + set_config('stackapi', false, 'qtype_stack'); - // Check results when using answertest summary in defaults. - set_config('stackapi', true, 'qtype_stack'); - StackQuestionLoader::$defaults = Yaml::parseFile(__DIR__ . '/fixtures/questiondefaultssugar.yml'); - $diff = StackQuestionLoader::detect_differences($infoxml); + // Input, blank specific feedback. + $xml = stack_api_test_data::get_question_string('inputblankspecific'); + $expected = [ + 'name' => 'Default', + 'questiontext' => 'Question wording [[input:ans1]] [[validation:ans1]]', + 'defaultgrade' => '2', + 'specificfeedback' => '', + 'questionsimplify' => '1', + 'input' => [ + [ + 'name' => 'ans1', + 'type' => 'algebraic', + 'tans' => 'ta1', + 'forbidfloat' => '1', + 'requirelowestterms' => '0', + 'checkanswertype' => '0', + 'mustverify' => '1', + 'showvalidation' => '1', + ], + ], + 'prt' => [], + ]; + $diff = StackQuestionLoader::detect_differences($xml); $diffarray = Yaml::parse($diff); - $this->assertEquals(5, count($diffarray)); + $this->assertEquals(7, count($diffarray)); + $this->assertEqualsCanonicalizing($expected, $diffarray); + + // No input, no specific feedback. + $xml = stack_api_test_data::get_question_string('noinputnospecific'); + $expected = [ + 'name' => 'Default', + 'questiontext' => 'Question wording', + 'defaultgrade' => '0', + 'questionsimplify' => '1', + 'input' => [], + 'prt' => [], + ]; + $diff = StackQuestionLoader::detect_differences($xml); + $diffarray = Yaml::parse($diff); + $this->assertEquals(7, count($diffarray)); $this->assertEqualsCanonicalizing($expected, $diffarray); set_config('stackapi', false, 'qtype_stack'); } @@ -708,4 +784,82 @@ public function test_split_answertest_spaces(): void { ]; $this->assertEquals($expected, StackQuestionLoader::split_answertest($input)); } + + public function test_question_loader_default_emptygrade(): void { + $xml = stack_api_test_data::get_question_string('emptygrade'); + $question = StackQuestionLoader::loadXML($xml)['question']; + $this->assertEquals( + '

Default question

[[input:ans1]] [[validation:ans1]]

', + $question->questiontext + ); + $this->assertEquals(2, $question->defaultmark); + $this->assertEquals( + '[[feedback:prt1]]', + $question->specificfeedback + ); + + $this->assertEquals(1, count($question->prts)); + $nodesummary = $question->prts['prt1']->get_nodes_summary()[0]; + $this->assertEquals('ATAlgEquiv(ans1,ta1)', $nodesummary->answertest); + $this->assertEquals(1, count($question->inputs)); + $this->assertEquals( + $question->inputs['ans1']->get_parameter('showValidation'), + get_config('qtype_stack', 'inputshowvalidation') + ); + } + + public function test_question_loader_default_noinputblankspecific(): void { + $xml = stack_api_test_data::get_question_string('noinputblankspecific'); + $question = StackQuestionLoader::loadXML($xml)['question']; + $this->assertEquals( + 'Question wording', + $question->questiontext + ); + $this->assertEquals(0, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback + ); + + $this->assertEquals(0, count($question->prts)); + $this->assertEquals(0, count($question->inputs)); + } + + public function test_question_loader_default_inputblankspecific(): void { + $xml = stack_api_test_data::get_question_string('inputblankspecific'); + $question = StackQuestionLoader::loadXML($xml)['question']; + $this->assertEquals( + 'Question wording [[input:ans1]] [[validation:ans1]]', + $question->questiontext + ); + $this->assertEquals(2, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback + ); + + $this->assertEquals(0, count($question->prts)); + $this->assertEquals(1, count($question->inputs)); + $this->assertEquals( + $question->inputs['ans1']->get_parameter('showValidation'), + get_config('qtype_stack', 'inputshowvalidation') + ); + } + + public function test_question_loader_default_noinputnospecific(): void { + $xml = stack_api_test_data::get_question_string('noinputnospecific'); + $question = StackQuestionLoader::loadXML($xml)['question']; + $this->assertEquals( + 'Question wording', + $question->questiontext + ); + $this->assertEquals(0, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback + ); + + $this->assertEquals(0, count($question->prts)); + $this->assertEquals(0, count($question->inputs)); + } } diff --git a/tests/fixtures/apifixtures.class.php b/tests/fixtures/apifixtures.class.php index fdedec264dd..fc48e1d5b95 100644 --- a/tests/fixtures/apifixtures.class.php +++ b/tests/fixtures/apifixtures.class.php @@ -31,6 +31,45 @@ class stack_api_test_data { ', + 'emptygrade' => + ' + + 2 + + ', + 'noinputblankspecific' => + ' + + + Question wording + + + + + 2 + + ', + 'inputblankspecific' => + ' + + + Question wording [[input:ans1]] [[validation:ans1]] + + + + + 2 + + ', + 'noinputnospecific' => + ' + + + Question wording + + 2 + + ', 'matrices' => ' diff --git a/tests/fixtures/questiondefaultssugar.yml b/tests/fixtures/questiondefaultssugar.yml index 15da78b02b8..9c27b82f23f 100644 --- a/tests/fixtures/questiondefaultssugar.yml +++ b/tests/fixtures/questiondefaultssugar.yml @@ -9,7 +9,7 @@ question: penalty: '0.1' hidden: '0' idnumber: '' - stackversion: '2025042500' + stackversion: '' questionvariables: 'ta1:1;' specificfeedback: '

[[feedback:prt1]]

' specificfeedbackformat: html diff --git a/tests/questiontype_test.php b/tests/questiontype_test.php index 3a6e8c96231..5625f5980be 100644 --- a/tests/questiontype_test.php +++ b/tests/questiontype_test.php @@ -779,4 +779,139 @@ public function test_import_xml_empty_fragment(): void { $this->assertEquals(\get_config('qtype_stack', 'inputforbidwords'), $question->ans1forbidwords); $this->assertEquals(\get_config('qtype_stack', 'inputboxsize'), $question->ans1boxsize); } + + public function test_import_xml_default_emptygrade(): void { + $xml = ' + + Default + + 2 + '; + if (class_exists('\core\xml_parser') && method_exists('\core\xml_parser', 'parse')) { + $parser = new \core\xml_parser(); + $xmldata = $parser->parse($xml); + } else { + $xmldata = xmlize($xml); + } + + $importer = new qformat_xml(); + $question = $importer->try_importing_using_qtypes($xmldata['question'], null, null, 'stack'); + $this->assertEquals( + '

Default question

[[input:ans1]] [[validation:ans1]]

', + $question->questiontext + ); + $this->assertEquals(2, $question->defaultmark); + $this->assertEquals( + '[[feedback:prt1]]', + $question->specificfeedback['text'] + ); + + $this->assertEquals('AlgEquiv', $question->prt1answertest[0]); + $this->assertEquals('algebraic', $question->ans1type); + } + + public function test_import_xml_default_noinputblankspecific(): void { + $xml = ' + + Default + + + Question wording + + + + + 2 + '; + if (class_exists('\core\xml_parser') && method_exists('\core\xml_parser', 'parse')) { + $parser = new \core\xml_parser(); + $xmldata = $parser->parse($xml); + } else { + $xmldata = xmlize($xml); + } + + $importer = new qformat_xml(); + $question = $importer->try_importing_using_qtypes($xmldata['question'], null, null, 'stack'); + $this->assertEquals( + 'Question wording', + $question->questiontext + ); + $this->assertEquals(0, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback['text'] + ); + + $this->assertEquals(null, $question->prt1answertest[0] ?? null); + $this->assertEquals(null, $question->ans1type ?? null); + } + + public function test_import_xml_default_inputblankspecific(): void { + $xml = ' + + Default + + + Question wording [[input:ans1]] [[validation:ans1]] + + + + + 2 + '; + if (class_exists('\core\xml_parser') && method_exists('\core\xml_parser', 'parse')) { + $parser = new \core\xml_parser(); + $xmldata = $parser->parse($xml); + } else { + $xmldata = xmlize($xml); + } + + $importer = new qformat_xml(); + $question = $importer->try_importing_using_qtypes($xmldata['question'], null, null, 'stack'); + $this->assertEquals( + 'Question wording [[input:ans1]] [[validation:ans1]]', + $question->questiontext + ); + $this->assertEquals(2, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback['text'] + ); + + $this->assertEquals(null, $question->prt1answertest[0] ?? null); + $this->assertEquals('algebraic', $question->ans1type); + } + + public function test_import_xml_default_noinputnospecific(): void { + $xml = ' + + Default + + + Question wording + + 2 + '; + if (class_exists('\core\xml_parser') && method_exists('\core\xml_parser', 'parse')) { + $parser = new \core\xml_parser(); + $xmldata = $parser->parse($xml); + } else { + $xmldata = xmlize($xml); + } + + $importer = new qformat_xml(); + $question = $importer->try_importing_using_qtypes($xmldata['question'], null, null, 'stack'); + $this->assertEquals( + 'Question wording', + $question->questiontext + ); + $this->assertEquals(0, $question->defaultmark); + $this->assertEquals( + '', + $question->specificfeedback['text'] + ); + + $this->assertEquals(null, $question->prt1answertest[0] ?? null); + $this->assertEquals(null, $question->ans1type ?? null); + } } From 441f20e88a41d7dd0446644406bdfea9088ed99e Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 13 Feb 2026 14:55:21 +0000 Subject: [PATCH 6/7] iss1678 - Code tidy --- api/util/StackQuestionLoader.php | 17 +++++++++-------- tests/fixtures/apifixtures.class.php | 8 ++++---- tests/questiontype_test.php | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index 3644d9d6eb3..92547534fa5 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -779,10 +779,11 @@ public static function detect_differences($xml) { self::get_default('question', 'specificfeedback', '[[feedback:prt1]]') ); $isrequesteddefaultprt = isset($plaindata['question']['questiontext']) && - isset($plaindata['question']['specificfeedback']) && preg_match( - "/\[\[feedback:{" . self::get_default('prt', 'name', 'prt1') . "}\]\]/", - $plaindata['question']['questiontext'] . $plaindata['question']['specificfeedback'] - ); + isset($plaindata['question']['specificfeedback']) && + preg_match( + "/\[\[feedback:{" . self::get_default('prt', 'name', 'prt1') . "}\]\]/", + $plaindata['question']['questiontext'] . $plaindata['question']['specificfeedback'] + ); if (!empty($plaindata['question']['input'])) { $diffinputs = []; foreach ($plaindata['question']['input'] as $input) { @@ -790,8 +791,8 @@ public static function detect_differences($xml) { $diffinputs[] = $diffinput; } $diff['input'] = $diffinputs; - // We need to create an input if questiontext contains [[input:ansnamedefault]] or - // questiontext doesn't exist and default contains [[input:ansnamedefault]]. + // We need to create an input if questiontext contains [[input:ansnamedefault]] or + // questiontext doesn't exist and default contains [[input:ansnamedefault]]. } else if ((!$isquestiontext && $isdefaultinput) || $isrequesteddefaultinput) { $diff['input'] = [['name' => self::get_default('input', 'name', 'ans1'), 'type' => self::get_default('input', 'type', 'algebraic'), @@ -846,8 +847,8 @@ public static function detect_differences($xml) { $diffprts[] = $diffprt; } $diff['prt'] = $diffprts; - // We need to create a PRT if questiontext contains [[input:ansnamedefault]] or - // questiontext doesn't exist and default contains [[input:ansnamedefault]]. + // We need to create a PRT if questiontext contains [[input:ansnamedefault]] or + // questiontext doesn't exist and default contains [[input:ansnamedefault]]. } else if ( ((!$isfeedback && $isdefaultprt) || $isrequesteddefaultprt) && ((!$isquestiontext && $isdefaultinput) || $isrequesteddefaultinput) diff --git a/tests/fixtures/apifixtures.class.php b/tests/fixtures/apifixtures.class.php index fc48e1d5b95..c39bba24765 100644 --- a/tests/fixtures/apifixtures.class.php +++ b/tests/fixtures/apifixtures.class.php @@ -33,10 +33,10 @@ class stack_api_test_data {
', 'emptygrade' => ' - - 2 - - ', + + 2 + + ', 'noinputblankspecific' => ' diff --git a/tests/questiontype_test.php b/tests/questiontype_test.php index 5625f5980be..ff587ebb5d0 100644 --- a/tests/questiontype_test.php +++ b/tests/questiontype_test.php @@ -780,7 +780,7 @@ public function test_import_xml_empty_fragment(): void { $this->assertEquals(\get_config('qtype_stack', 'inputboxsize'), $question->ans1boxsize); } - public function test_import_xml_default_emptygrade(): void { + public function test_import_xml_default_emptygrade(): void { $xml = ' Default From 6b1a8711916df0a7eed9857552443f7b7c9b1ddd Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 16 Feb 2026 14:38:15 +0000 Subject: [PATCH 7/7] iss1678 - Fix tests --- questiontype.php | 2 +- tests/api_stackquestionloader_test.php | 6 ++++-- tests/fixtures/questiondefaultssugar.yml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/questiontype.php b/questiontype.php index 57b1d865b8f..80bdab6ba3a 100644 --- a/questiontype.php +++ b/questiontype.php @@ -2148,7 +2148,7 @@ protected function import_xml_qtest($xml, qformat_xml $format) { if (isset($xml['#']['testinput'])) { foreach ($xml['#']['testinput'] as $inputxml) { $name = $format->getpath($inputxml, ['#', 'name', 0, '#'], 'ans1'); - $value = $format->getpath($inputxml, ['#', 'value', 0, '#'], 'ta1'); + $value = $format->getpath($inputxml, ['#', 'value', 0, '#'], ''); $inputs[$name] = $value; } } diff --git a/tests/api_stackquestionloader_test.php b/tests/api_stackquestionloader_test.php index 89e91edf1b3..c762f9368e7 100644 --- a/tests/api_stackquestionloader_test.php +++ b/tests/api_stackquestionloader_test.php @@ -499,6 +499,7 @@ public function test_detect_difference_yml(): void { 'testinput' => [ [ 'name' => 'ans1', + 'value' => 'ta1', ], [ 'name' => 'ans2', @@ -535,7 +536,8 @@ public function test_detect_difference_yml(): void { "value: '1.0000001'\n autosimplify: '1'\n feedbackstyle: '1'\n node:\n - name: '0'\n " . "answertest: AlgEquiv\n sans: ans2\n tans: ta2\n quiet: '0'\n falsescore: '1'\n" . "deployedseed:\n - '1'\n - '2'\n - '3'\nqtest:\n - testcase: '1'\n description: 'A test'\n " . - "testinput:\n - name: ans1\n - name: ans2\n value: ta2\n expected:\n - name: prt1" . + "testinput:\n - name: ans1\n value: ta1\n - name: ans2\n" . + " value: ta2\n expected:\n - name: prt1" . "\n expectedscore: '1.0000000'\n expectedpenalty: '0.0000000'\n " . "- name: prt2\n expectedscore: '1.0000000'\n expectedpenalty:" . " '0.0000000'\n expectedanswernote: 2-0-T\n"; @@ -725,7 +727,7 @@ public function test_detect_difference_yml(): void { ]; $diff = StackQuestionLoader::detect_differences($xml); $diffarray = Yaml::parse($diff); - $this->assertEquals(7, count($diffarray)); + $this->assertEquals(6, count($diffarray)); $this->assertEqualsCanonicalizing($expected, $diffarray); set_config('stackapi', false, 'qtype_stack'); } diff --git a/tests/fixtures/questiondefaultssugar.yml b/tests/fixtures/questiondefaultssugar.yml index 9c27b82f23f..a7186b487ce 100644 --- a/tests/fixtures/questiondefaultssugar.yml +++ b/tests/fixtures/questiondefaultssugar.yml @@ -89,7 +89,7 @@ qtest: description: testinput: name: 'ans1' - value: 'ta1' + value: '' expected: name: 'prt1' expectedscore: