diff --git a/classes/form/context/array_render_context.php b/classes/form/context/array_render_context.php index 7b2d2990..1d33bb3f 100644 --- a/classes/form/context/array_render_context.php +++ b/classes/form/context/array_render_context.php @@ -67,7 +67,7 @@ class array_render_context extends render_context { * Initializes a new array-based context. * * @param render_context $parent context containing this group - * @param string $prefix prefix for the names of elements in this context + * @param string $prefix prefix for the names of elements in this context */ public function __construct(render_context $parent, string $prefix) { $this->parent = $parent; @@ -82,8 +82,8 @@ public function __construct(render_context $parent, string $prefix) { /** * Create, add and return an element. * - * @param string $type the type name of the element, as per the Moodle docs. - * @param string $name the name of the generated form element. + * @param string $type the type name of the element, as per the Moodle docs. + * @param string $name the name of the generated form element. * @param mixed ...$args remaining arguments specific to the element type. * @return object the created element. Really an instance of {@see \HTML_QuickForm_element}, but the return type of * {@see \MoodleQuickForm::addElement()} is also an object. @@ -109,7 +109,7 @@ public function set_type(string $name, string $type): void { /** * Sets the default of an element which has been (or will be) added independently. * - * @param string $name the name of the target element. + * @param string $name the name of the target element. * @param mixed $default default value for the element. * @see \MoodleQuickForm::setDefault() */ @@ -122,14 +122,14 @@ public function set_default(string $name, $default): void { * * Must be called *after* the element was added using {@see add_element}. * - * @param string $name the name of the target element. - * @param string|null $message message to display for invalid data. - * @param string $type rule type, use getRegisteredRules() to get types. - * @param string|null $format required for extra rule data. + * @param string $name the name of the target element. + * @param string|null $message message to display for invalid data. + * @param string $type rule type, use getRegisteredRules() to get types. + * @param string|null $format required for extra rule data. * @param string|null $validation where to perform validation: "server", "client". - * @param bool $reset client-side validation: reset the form element to its original value if there is + * @param bool $reset client-side validation: reset the form element to its original value if there is * an error? - * @param bool $force force the rule to be applied, even if the target form element does not exist. + * @param bool $force force the rule to be applied, even if the target form element does not exist. * @see \MoodleQuickForm::addRule() */ public function add_rule(string $name, ?string $message, string $type, ?string $format = null, @@ -144,9 +144,9 @@ public function add_rule(string $name, ?string $message, string $type, ?string $ * Adds a condition which will disable the named element if met. * * @param string $dependant name of the element which has the dependency on another element - * @param string $dependency absolute name of the element which is depended on - * @param string $operator one of a fixed set of conditions, as in {@see MoodleQuickForm::disabledIf} - * @param mixed $value for conditions requiring it, the value to compare with. Ignored otherwise. + * @param string $dependency absolute name of the element which is depended on + * @param string $operator one of a fixed set of conditions, as in {@see MoodleQuickForm::disabledIf} + * @param mixed $value for conditions requiring it, the value to compare with. Ignored otherwise. * @see \MoodleQuickForm::disabledIf() */ public function disable_if(string $dependant, string $dependency, string $operator, $value = null): void { @@ -157,9 +157,9 @@ public function disable_if(string $dependant, string $dependency, string $operat * Adds a condition which will hide the named element if met. * * @param string $dependant name of the element which has the dependency on another element - * @param string $dependency absolute name of the element which is depended on - * @param string $operator one of a fixed set of conditions, as in {@see MoodleQuickForm::hideIf} - * @param mixed $value for conditions requiring it, the value to compare with. Ignored otherwise. + * @param string $dependency absolute name of the element which is depended on + * @param string $operator one of a fixed set of conditions, as in {@see MoodleQuickForm::hideIf} + * @param mixed $value for conditions requiring it, the value to compare with. Ignored otherwise. * @see \MoodleQuickForm::hideIf() */ public function hide_if(string $dependant, string $dependency, string $operator, $value = null): void { @@ -187,4 +187,13 @@ public function next_unique_int(): int { public function contextualize(?string $text): ?string { return $this->parent->contextualize($text); } + + /** + * Generate a new UUID. Probably uses {@see uuid}, but may be overridden for tests. + * + * @return string + */ + public function generate_uuid(): string { + return $this->parent->generate_uuid(); + } } diff --git a/classes/form/context/mform_render_context.php b/classes/form/context/mform_render_context.php index d15552e0..0559f4a0 100644 --- a/classes/form/context/mform_render_context.php +++ b/classes/form/context/mform_render_context.php @@ -16,6 +16,7 @@ namespace qtype_questionpy\form\context; +use Closure; use qtype_questionpy\utils; /** diff --git a/classes/form/context/render_context.php b/classes/form/context/render_context.php index 8fc6f827..67893798 100644 --- a/classes/form/context/render_context.php +++ b/classes/form/context/render_context.php @@ -16,6 +16,7 @@ namespace qtype_questionpy\form\context; +use core\uuid; use moodleform; use MoodleQuickForm; use qtype_questionpy\utils; @@ -212,4 +213,11 @@ public function reference_to_absolute(string $reference): string { * @return string|null input string with format specifiers replaced */ abstract public function contextualize(?string $text): ?string; + + /** + * Generate a new UUID. Probably uses {@see uuid}, but may be overridden for tests. + * + * @return string + */ + abstract public function generate_uuid(): string; } diff --git a/classes/form/context/root_render_context.php b/classes/form/context/root_render_context.php index 7165df4f..64c8531c 100644 --- a/classes/form/context/root_render_context.php +++ b/classes/form/context/root_render_context.php @@ -16,6 +16,8 @@ namespace qtype_questionpy\form\context; +use core\uuid; + /** * Uppermost render context. * @@ -28,6 +30,9 @@ class root_render_context extends mform_render_context { /** @var int the next int which will be returned by {@see next_unique_int} */ private int $nextuniqueint = 1; + /** @var callable can be set from tests to supply mock UUIDs */ + public $uuidgen = [uuid::class, 'generate']; + /** * Get a unique and deterministic integer for use in generated element names and IDs. * @@ -48,4 +53,13 @@ public function next_unique_int(): int { public function contextualize(?string $text): ?string { return $text; } + + /** + * Generate a new UUID. Probably uses {@see uuid}, but may be overridden for tests. + * + * @return string + */ + public function generate_uuid(): string { + return ($this->uuidgen)(); + } } diff --git a/classes/form/context/section_render_context.php b/classes/form/context/section_render_context.php index 0c885d20..46505436 100644 --- a/classes/form/context/section_render_context.php +++ b/classes/form/context/section_render_context.php @@ -36,7 +36,7 @@ class section_render_context extends mform_render_context { * Initializes a new {@see section_render_context}. * * @param render_context $parent context containing this section - * @param string $name the name part which will be appended to `$parent`'s prefix + * @param string $name the name part which will be appended to `$parent`'s prefix */ public function __construct(render_context $parent, string $name) { $this->parent = $parent; @@ -69,4 +69,13 @@ public function next_unique_int(): int { public function contextualize(?string $text): ?string { return $this->parent->contextualize($text); } + + /** + * Generate a new UUID. Probably uses {@see uuid}, but may be overridden for tests. + * + * @return string + */ + public function generate_uuid(): string { + return $this->parent->generate_uuid(); + } } diff --git a/classes/form/elements/form_element.php b/classes/form/elements/form_element.php index 852b3aee..fcc3ca0e 100644 --- a/classes/form/elements/form_element.php +++ b/classes/form/elements/form_element.php @@ -27,7 +27,7 @@ * @copyright 2022 TU Berlin, innoCampus {@link https://www.questionpy.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -#[array_polymorphic('kind', variants: [ +#[array_polymorphic(discriminator: 'kind', variants: [ 'checkbox' => checkbox_element::class, 'checkbox_group' => checkbox_group_element::class, 'group' => group_element::class, @@ -38,6 +38,7 @@ 'static_text' => static_text_element::class, 'input' => text_input_element::class, 'textarea' => text_area_element::class, + 'id' => generated_id_element::class, ], fallbackvariant: fallback_element::class)] abstract class form_element implements qpy_renderable { } diff --git a/classes/form/elements/generated_id_element.php b/classes/form/elements/generated_id_element.php new file mode 100644 index 00000000..11d3c2ac --- /dev/null +++ b/classes/form/elements/generated_id_element.php @@ -0,0 +1,60 @@ +. + +namespace qtype_questionpy\form\elements; + +use core\uuid; +use qtype_questionpy\form\context\render_context; + + +/** + * Generates a unique ID which won't change across form saves. + * + * @package qtype_questionpy + * @author Maximilian Haye + * @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class generated_id_element extends form_element { + /** + * Trivial constructor. + * + * @param string $name + */ + public function __construct( + /** @var string $name */ + public string $name + ) { + } + + /** + * Render this item to the given context. + * + * @param render_context $context target context + * @package qtype_questionpy + */ + public function render_to(render_context $context): void { + $mangledname = $context->mangle_name($this->name); + $value = $context->moodleform->optional_param( + $mangledname, + null, + PARAM_ALPHANUMEXT + ) ?? $context->data[$this->name] ?? $context->generate_uuid(); + + $context->add_element('hidden', $mangledname, $value); + $context->set_type($this->name, PARAM_ALPHANUMEXT); + } +} diff --git a/tests/data_provider.php b/tests/data_provider.php index a35b4c97..f7b1b130 100644 --- a/tests/data_provider.php +++ b/tests/data_provider.php @@ -33,6 +33,7 @@ use qtype_questionpy\form\conditions\is_not_checked; use qtype_questionpy\form\elements\checkbox_element; use qtype_questionpy\form\elements\checkbox_group_element; +use qtype_questionpy\form\elements\generated_id_element; use qtype_questionpy\form\elements\group_element; use qtype_questionpy\form\elements\hidden_element; use qtype_questionpy\form\elements\option; @@ -143,5 +144,6 @@ function element_provider(): array { ['static_text', new static_text_element('my_text', 'Label', 'Lorem ipsum dolor sit amet.')], ['input', new text_input_element('my_field', 'Label', true, 'default', 'placeholder')], ['textarea', new text_area_element('my_field', 'Label', true, 'default', 'placeholder')], + ['id', new generated_id_element('my_id')], ]; } diff --git a/tests/form/elements/html/id.html b/tests/form/elements/html/id.html new file mode 100644 index 00000000..32c6c09a --- /dev/null +++ b/tests/form/elements/html/id.html @@ -0,0 +1,9 @@ + +
+
+ + +
+ + +
diff --git a/tests/form/elements/json/id.json b/tests/form/elements/json/id.json new file mode 100644 index 00000000..9f3332b8 --- /dev/null +++ b/tests/form/elements/json/id.json @@ -0,0 +1,4 @@ +{ + "kind": "id", + "name": "my_id" +} diff --git a/tests/form/elements/test_moodleform.php b/tests/form/elements/test_moodleform.php index 76b62380..cfb19fae 100644 --- a/tests/form/elements/test_moodleform.php +++ b/tests/form/elements/test_moodleform.php @@ -55,6 +55,7 @@ public function __construct(qpy_renderable $element) { */ protected function definition() { $context = new root_render_context($this, $this->_form, 'qpy_form', []); + $context->uuidgen = fn() => '24daab97-7eeb-422d-a2f3-f4e770fb11f6'; $this->element->render_to($context); } }