Skip to content

Commit f72edf5

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 b4fc264 commit f72edf5

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
@@ -124,7 +124,7 @@ public static function set_select_value(DOMElement $select, string $value): void
124124
} catch (DOMException $e) {
125125
// Thrown by createElementNS "If invalid $namespace or $qualifiedName", which are both constants, so
126126
// the coding_exception fits.
127-
throw new \coding_exception($e->getMessage());
127+
throw new coding_exception($e->getMessage());
128128
}
129129
if (!$fallbackoption) {
130130
debugging("Could not add fallback option element for value '$value', which is no longer available.");
@@ -135,4 +135,28 @@ public static function set_select_value(DOMElement $select, string $value): void
135135
$select->appendChild($fallbackoption);
136136
}
137137
}
138+
139+
/**
140+
* Appends an XHTML hidden input to the given element.
141+
*
142+
* @param DOMElement $parent
143+
* @param string $name
144+
* @param string $value
145+
* @return DOMElement
146+
* @throws coding_exception
147+
*/
148+
public static function add_hidden_input(DOMElement $parent, string $name, string $value): DOMElement {
149+
try {
150+
$element = $parent->ownerDocument->createElementNS(constants::NAMESPACE_XHTML, 'input');
151+
} catch (DOMException $e) {
152+
// Thrown by createElementNS "If invalid $namespace or $qualifiedName", which are both constants, so
153+
// the coding_exception fits.
154+
throw new coding_exception($e->getMessage());
155+
}
156+
$element->setAttribute('type', 'hidden');
157+
$element->setAttribute('name', $name);
158+
$element->setAttribute('value', $value);
159+
$parent->appendChild($element);
160+
return $element;
161+
}
138162
}

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
}
@@ -601,11 +601,11 @@ function (array $match) use ($question) {
601601
* - While we should discourage it, it is possible for inputs to be inside `qpy:if-role` or `qpy:feedback`
602602
* elements. {@see question_ui_metadata_extractor} doesn't resolve those.
603603
*
604-
* @return array
605-
* @see check_for_unknown_options
604+
* @return array<string, available_opts_info>
605+
* @see check_for_and_preserve_unknown_options
606606
*/
607607
private function extract_available_options(): array {
608-
$optionsbyname = [];
608+
$infobyname = [];
609609

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

617-
$values = [];
617+
$optvalues = [];
618618
/** @var DOMElement $option */
619619
foreach ($this->xpath->query('./xhtml:option | ./xhtml:optgroup/xhtml:option', $select) as $option) {
620-
$values[] = $option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent;
620+
$optvalues[] = $option->hasAttribute('value') ? $option->getAttribute('value') : $option->textContent;
621621
}
622622

623-
$optionsbyname[$name] = array_unique($values);
623+
$warn = $select->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') !== 'no';
624+
625+
$infobyname[$name] = new available_opts_info('select', array_unique($optvalues), $warn);
624626
}
625627

626-
$ignorednames = [];
627628
/** @var DOMElement $input */
628629
foreach ($this->xpath->query('//xhtml:input[(@type="checkbox" or @type="radio")]') as $input) {
629630
$name = $input->getAttribute('name');
630631
if (!$name) {
631632
continue;
632633
}
633-
if (in_array($name, $ignorednames)) {
634-
continue;
635-
}
636-
if ($input->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') === 'no') {
637-
$ignorednames[] = $name;
638-
continue;
639-
}
640634

641-
if (!array_key_exists($name, $optionsbyname)) {
642-
$optionsbyname[$name] = [];
635+
$info = $infobyname[$name] ??= new available_opts_info($input->getAttribute('type'), [], true);
636+
637+
if ($input->getAttributeNS(constants::NAMESPACE_QPY, 'warn-on-unknown-option') === 'no') {
638+
$info->warnonunknownoption = false;
643639
}
644640

645641
$value = $input->hasAttribute('value') ? $input->getAttribute('value') : 'on';
646-
if (!in_array($value, $optionsbyname[$name])) {
647-
$optionsbyname[$name][] = $value;
642+
if (!in_array($value, $info->availableoptions)) {
643+
$info->availableoptions[] = $value;
648644
}
649645
}
650646

651-
foreach ($ignorednames as $ignoredname) {
652-
unset($optionsbyname[$ignoredname]);
653-
}
654-
foreach ($optionsbyname as &$values) {
655-
sort($values);
647+
foreach ($infobyname as $info) {
648+
sort($info->availableoptions);
656649
}
657650

658-
return $optionsbyname;
651+
return $infobyname;
659652
}
660653

661654
/**
662-
* Checks if the last response contains values which are invalid.
655+
* Checks the last response for invalid values and adds hidden inputs to preserve those invalid values.
663656
*
664-
* @param array $availableoptionsbyname
665-
* @return array
657+
* @param available_opts_info[] $availableoptsinfobyname
658+
* @return invalid_option_warning[]
659+
* @throws coding_exception
666660
* @see extract_available_options
667661
*/
668-
private function check_for_unknown_options(array $availableoptionsbyname): array {
662+
private function check_for_and_preserve_unknown_options(array $availableoptsinfobyname): array {
669663
$response = utils::get_last_response($this->attempt);
670664

671665
$warnings = [];
672-
foreach ($availableoptionsbyname as $name => $availableoptions) {
666+
foreach ($availableoptsinfobyname as $name => $info) {
667+
if (!$info->warnonunknownoption) {
668+
continue;
669+
}
673670
if (!array_key_exists($name, $response)) {
674671
continue;
675672
}
676673

677674
$lastvalue = $response[$name];
678-
if (!in_array($lastvalue, $availableoptions)) {
679-
$warnings[] = new invalid_option_warning($name, $lastvalue, $availableoptions);
675+
if (in_array($lastvalue, $info->availableoptions)) {
676+
continue;
677+
}
678+
679+
$warnings[] = new invalid_option_warning($name, $lastvalue, $info->availableoptions);
680+
if ($info->type !== 'select') {
681+
// Selects are handled in dom_utils::set_select_value.
682+
dom_utils::add_hidden_input($this->xml->documentElement, $this->attempt->get_qt_field_name($name), $lastvalue);
680683
}
681684
}
682685
return $warnings;

Diff for: renderer.php

-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ public function formulation_and_controls(question_attempt $qa, question_display_
7474
$isstudent = $qa->get_step(0)->get_user_id() === $USER->id;
7575
$output .= $this->output->render_from_template('qtype_questionpy/render_warnings', [
7676
'warnings' => $renderresult->warnings,
77-
'get_qt_field_name' => fn($text, $render) => $qa->get_qt_field_name($render(trim($text))),
7877
'should_use_list' => count($renderresult->warnings) > 1,
7978
'should_show_hint_contact_trainers' => $isstudent,
8079
'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)