diff --git a/api/questiondefaults.yml b/api/questiondefaults.yml index 280659a31ae..91477c050d7 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 0df570b1f97..34fb042bc4d 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -138,10 +138,15 @@ 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 { + if (preg_match("/\[\[input:" . self::get_default('input', 'name', 'ans1') . "\]\]/", $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'] : @@ -295,11 +300,17 @@ public static function loadxml($xml, $includetests = false) { $inputmap[(string) $input->name] = $input; } - 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; + if (empty($inputmap)) { + $inputname = self::get_default('input', 'name', 'ans1'); + if (preg_match("/\[\[input:{$inputname}\]\]/", $question->questiontext)) { + $defaultinput = new \SimpleXMLElement(''); + $defaultinput->addChild('name', $inputname); + $defaultinput->addChild('tans', self::get_default('input', 'tans', 'ta1')); + $inputmap[$inputname] = $defaultinput; + } else { + // We've not got any inputs. Set default mark to 0. + $question->defaultmark = 0; + } } $requiredparams = \stack_input_factory::get_parameters_used(); @@ -372,16 +383,19 @@ public static function loadxml($xml, $includetests = false) { $prtmap[(string) $prt->name] = $prt; } - if (empty($prtmap) && $question->defaultmark) { - $defaultprt = new \SimpleXMLElement('Default question
[[input:ans1]] [[validation: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') . "\]\]/", + 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'] + ); if (!empty($plaindata['question']['input'])) { $diffinputs = []; foreach ($plaindata['question']['input'] as $input) { @@ -756,7 +791,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'), @@ -767,6 +804,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 = []; @@ -805,12 +847,17 @@ 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') { - $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/questiontype.php b/questiontype.php index 6bd3531cb1a..80bdab6ba3a 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; @@ -1875,10 +1885,13 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = $this->import_xml_input($inputxml, $fromform, $format); } } else { - if ($fromform->defaultmark) { + 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; } } @@ -1887,7 +1900,7 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = $structurerepairs .= $this->import_xml_prt($prtxml, $fromform, $format); } } else { - if ($fromform->defaultmark) { + if (preg_match("/\[\[feedback:prt1\]\]/", $fromform->questiontext . $fromform->specificfeedback['text'])) { $defaultnode = [ 'name' => [0 => ['#' => 0]], 'sans' => [0 => ['#' => 'ans1']], @@ -2135,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 abdf30a9afe..c762f9368e7 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.
', @@ -498,6 +499,7 @@ public function test_detect_difference_yml(): void { 'testinput' => [ [ 'name' => 'ans1', + 'value' => 'ta1', ], [ 'name' => 'ans2', @@ -521,7 +523,7 @@ public function test_detect_difference_yml(): void { ], ]; $expectedstring = "name: 'Test question'\nquestiontext: |\nQuestion
[[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 " . @@ -534,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"; @@ -546,7 +549,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 +599,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 +618,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 = '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 df12c590333..d363803a556 100644 --- a/tests/fixtures/apifixtures.class.php +++ b/tests/fixtures/apifixtures.class.php @@ -31,6 +31,45 @@ class stack_api_test_data {[[feedback:prt1]]
' specificfeedbackformat: html @@ -89,7 +89,7 @@ qtest: description: testinput: name: 'ans1' - value: 'ta1' + value: '' expected: name: 'prt1' expectedscore: diff --git a/tests/questiontype_test.php b/tests/questiontype_test.php index 3a6e8c96231..ff587ebb5d0 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 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 = '