Skip to content

Commit 29df421

Browse files
committed
refactor: add hidden inputs in question_ui_renderer, don't add one for selects
Selects already preserve their value through the added fallback option.
1 parent c7fd466 commit 29df421

File tree

5 files changed

+105
-40
lines changed

5 files changed

+105
-40
lines changed

Diff for: classes/attempt_ui/available_opts_info.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace qtype_questionpy\attempt_ui;
18+
19+
/**
20+
* Used by {@see question_ui_renderer::extract_available_options()} to hold type, available options and warning opt-out.
21+
*
22+
* @package qtype_questionpy
23+
* @author Maximilian Haye
24+
* @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org}
25+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26+
*/
27+
class available_opts_info {
28+
/**
29+
* Trivial constructor.
30+
*
31+
* @param string $type
32+
* @param array $availableoptions
33+
* @param bool $warnonunknownoption
34+
*/
35+
public function __construct(
36+
/** @var string $type `radio`, `checkbox` or `select` */
37+
public string $type,
38+
/** @var string[] $availableoptions */
39+
public array $availableoptions,
40+
/** @var bool $warnonunknownoption */
41+
public bool $warnonunknownoption
42+
) {
43+
}
44+
}

Diff for: classes/attempt_ui/dom_utils.php

+25-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public static function set_select_value(DOMElement $select, string $value): void
115115
} catch (DOMException $e) {
116116
// Thrown by createElementNS "If invalid $namespace or $qualifiedName", which are both constants, so
117117
// the coding_exception fits.
118-
throw new \coding_exception($e->getMessage());
118+
throw new coding_exception($e->getMessage());
119119
}
120120
if (!$fallbackoption) {
121121
debugging("Could not add fallback option element for value '$value', which is no longer available.");
@@ -126,4 +126,28 @@ public static function set_select_value(DOMElement $select, string $value): void
126126
$select->appendChild($fallbackoption);
127127
}
128128
}
129+
130+
/**
131+
* Appends an XHTML hidden input to the given element.
132+
*
133+
* @param DOMElement $parent
134+
* @param string $name
135+
* @param string $value
136+
* @return DOMElement
137+
* @throws coding_exception
138+
*/
139+
public static function add_hidden_input(DOMElement $parent, string $name, string $value): DOMElement {
140+
try {
141+
$element = $parent->ownerDocument->createElementNS(constants::NAMESPACE_XHTML, 'input');
142+
} catch (DOMException $e) {
143+
// Thrown by createElementNS "If invalid $namespace or $qualifiedName", which are both constants, so
144+
// the coding_exception fits.
145+
throw new coding_exception($e->getMessage());
146+
}
147+
$element->setAttribute('type', 'hidden');
148+
$element->setAttribute('name', $name);
149+
$element->setAttribute('value', $value);
150+
$parent->appendChild($element);
151+
return $element;
152+
}
129153
}

Diff for: classes/attempt_ui/question_ui_renderer.php

+35-32
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function render(): render_result {
134134
mt_srand($nextseed);
135135
}
136136

137-
$warnings = $this->check_for_unknown_options($availableoptions);
137+
$warnings = $this->check_for_and_preserve_unknown_options($availableoptions);
138138
$this->result = new render_result($this->xml->saveHTML(), $warnings);
139139
return $this->result;
140140
}
@@ -595,11 +595,11 @@ function (array $match) use ($question) {
595595
* - While we should discourage it, it is possible for inputs to be inside `qpy:if-role` or `qpy:feedback`
596596
* elements. {@see question_ui_metadata_extractor} doesn't resolve those.
597597
*
598-
* @return array
599-
* @see check_for_unknown_options
598+
* @return array<string, available_opts_info>
599+
* @see check_for_and_preserve_unknown_options
600600
*/
601601
private function extract_available_options(): array {
602-
$optionsbyname = [];
602+
$infobyname = [];
603603

604604
/** @var DOMElement $select */
605605
foreach ($this->xpath->query('//xhtml:select[not(@qpy:warn-on-unknown-option = "no")]') as $select) {
@@ -608,69 +608,72 @@ private function extract_available_options(): array {
608608
continue;
609609
}
610610

611-
$values = [];
611+
$optvalues = [];
612612
/** @var DOMElement $option */
613613
foreach ($this->xpath->query('./xhtml:option | ./xhtml:optgroup/xhtml:option', $select) as $option) {
614-
$values[] = $option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent;
614+
$optvalues[] = $option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent;
615615
}
616616

617-
$optionsbyname[$name] = array_unique($values);
617+
$warn = $select->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') !== 'no';
618+
619+
$infobyname[$name] = new available_opts_info('select', array_unique($optvalues), $warn);
618620
}
619621

620-
$ignorednames = [];
621622
/** @var DOMElement $input */
622623
foreach ($this->xpath->query('//xhtml:input[(@type="checkbox" or @type="radio")]') as $input) {
623624
$name = $input->getAttribute('name');
624625
if (!$name) {
625626
continue;
626627
}
627-
if (in_array($name, $ignorednames)) {
628-
continue;
629-
}
630-
if ($input->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') === 'no') {
631-
$ignorednames[] = $name;
632-
continue;
633-
}
634628

635-
if (!array_key_exists($name, $optionsbyname)) {
636-
$optionsbyname[$name] = [];
629+
$info = $infobyname[$name] ??= new available_opts_info($input->getAttribute('type'), [], true);
630+
631+
if ($input->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') === 'no') {
632+
$info->warnonunknownoption = false;
637633
}
638634

639635
$value = $input->hasAttribute('value') ? $input->getAttribute('value') : 'on';
640-
if (!in_array($value, $optionsbyname[$name])) {
641-
$optionsbyname[$name][] = $value;
636+
if (!in_array($value, $info->availableoptions)) {
637+
$info->availableoptions[] = $value;
642638
}
643639
}
644640

645-
foreach ($ignorednames as $ignoredname) {
646-
unset($optionsbyname[$ignoredname]);
647-
}
648-
foreach ($optionsbyname as &$values) {
649-
sort($values);
641+
foreach ($infobyname as $info) {
642+
sort($info->availableoptions);
650643
}
651644

652-
return $optionsbyname;
645+
return $infobyname;
653646
}
654647

655648
/**
656-
* Checks if the last response contains values which are invalid.
649+
* Checks the last response for invalid values and adds hidden inputs to preserve those invalid values.
657650
*
658-
* @param array $availableoptionsbyname
659-
* @return array
651+
* @param available_opts_info[] $availableoptsinfobyname
652+
* @return invalid_option_warning[]
653+
* @throws coding_exception
660654
* @see extract_available_options
661655
*/
662-
private function check_for_unknown_options(array $availableoptionsbyname): array {
656+
private function check_for_and_preserve_unknown_options(array $availableoptsinfobyname): array {
663657
$response = utils::get_last_response($this->attempt);
664658

665659
$warnings = [];
666-
foreach ($availableoptionsbyname as $name => $availableoptions) {
660+
foreach ($availableoptsinfobyname as $name => $info) {
661+
if (!$info->warnonunknownoption) {
662+
continue;
663+
}
667664
if (!array_key_exists($name, $response)) {
668665
continue;
669666
}
670667

671668
$lastvalue = $response[$name];
672-
if (!in_array($lastvalue, $availableoptions)) {
673-
$warnings[] = new invalid_option_warning($name, $lastvalue, $availableoptions);
669+
if (in_array($lastvalue, $info->availableoptions)) {
670+
continue;
671+
}
672+
673+
$warnings[] = new invalid_option_warning($name, $lastvalue, $info->availableoptions);
674+
if ($info->type !== 'select') {
675+
// Selects are handled in dom_utils::set_select_value.
676+
dom_utils::add_hidden_input($this->xml->documentElement, $this->attempt->get_qt_field_name($name), $lastvalue);
674677
}
675678
}
676679
return $warnings;

Diff for: renderer.php

-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ protected function formulation_controls_feedback_in_iframe(question_attempt $qa,
172172
$isstudent = $qa->get_step(0)->get_user_id() === $USER->id;
173173
$warningshtml .= $this->output->render_from_template('qtype_questionpy/render_warnings', [
174174
'warnings' => $renderresult->warnings,
175-
'get_qt_field_name' => fn($text, $render) => $qa->get_qt_field_name($render(trim($text))),
176175
'should_use_list' => count($renderresult->warnings) > 1,
177176
'should_show_hint_contact_trainers' => $isstudent,
178177
'should_show_hint_editable' => $isstudent && !$options->readonly,

Diff for: templates/render_warnings.mustache

+1-6
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,13 @@
3939
{{#should_use_list}}
4040
<ul>
4141
{{#warnings}}
42-
<li>
43-
{{{localize}}}
44-
<input type="hidden" name="{{#get_qt_field_name}} {{name}} {{/get_qt_field_name}}"
45-
value="{{value}}">
46-
</li>
42+
<li>{{{localize}}}</li>
4743
{{/warnings}}
4844
</ul>
4945
{{/should_use_list}}
5046
{{^should_use_list}}
5147
{{#warnings}}
5248
<p>{{{localize}}}</p>
53-
<input type="hidden" name="{{#get_qt_field_name}} {{name}} {{/get_qt_field_name}}" value="{{value}}">
5449
{{/warnings}}
5550
{{/should_use_list}}
5651

0 commit comments

Comments
 (0)