From c729e1479b765cf94e04d2d5f22278531fefcd5b Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 13 Feb 2026 15:14:01 +0000 Subject: [PATCH 1/4] nrw3 - Full HTML render --- api/controller/RenderController.php | 53 ++++++++++++++++++++++++++++- api/public/stackjsvle.js | 10 +++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/api/controller/RenderController.php b/api/controller/RenderController.php index fcef706e31b..03d5253c132 100644 --- a/api/controller/RenderController.php +++ b/api/controller/RenderController.php @@ -38,6 +38,7 @@ use api\util\StackSeedHelper; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Http as Uri; // phpcs:ignore moodle.Commenting.MissingDocblock.Class class RenderController { @@ -45,7 +46,6 @@ class RenderController { public function __invoke(Request $request, Response $response, array $args): Response { // TO-DO: Validate. $data = $request->getParsedBody(); - $question = StackQuestionLoader::loadxml($data["questionDefinition"])['question']; StackSeedHelper::initialize_seed($question, $data["seed"]); @@ -142,7 +142,58 @@ public function __invoke(Request $request, Response $response, array $args): Res $renderresponse->iframes = StackIframeHolder::$iframes; $renderresponse->isinteractive = $question->is_interactive(); + if ($data['fullRender']) { + // Request for full rendering. We replace placeholders with input renders and basic feedback and validation divs. + // Iframes are rendered but will still need to be registered on the front end. + $uri = $request->getUri(); + $baseurl = $uri->getScheme() . '://' . $uri->getHost(); + $port = $uri->getPort(); + if ($port && !in_array($port, [80, 443], true)) { + $baseurl .= ':' . $port; + } + + [$validationprefix, $feedbackprefix] = explode(',', $data['fullRender']); + $validationprefix = trim($validationprefix); + $feedbackprefix = trim($feedbackprefix); + preg_match_all('/\[\[input:([^\]]*)\]\]/', $renderresponse->questionrender, $inputtags); + foreach ($inputtags[1] as $tag) { + $renderresponse->questionrender = str_replace("[[input:{$tag}]]", $renderresponse->questioninputs->$tag->render, $renderresponse->questionrender); + $renderresponse->questionrender = str_replace("[[validation:{$tag}]]", "", $renderresponse->questionrender); + } + foreach ($renderresponse->iframes as $iframe) { + $iframe[1] = str_replace('', "", $iframe[1]); + $renderediframe = ""; + $renderresponse->questionrender = str_replace("id=\"{$iframe[2]}\">", "id=\"{$iframe[2]}\">{$renderediframe}", $renderresponse->questionrender); + $renderresponse->questionsamplesolutiontext = str_replace("id=\"{$iframe[2]}\">", "id=\"{$iframe[2]}\">{$renderediframe}", $renderresponse->questionsamplesolutiontext); + } + foreach ($renderresponse->questionassets as $name => $file) { + $renderresponse->questionrender = str_replace($name, "{$baseurl}/plots/{$file}", $renderresponse->questionrender); + $renderresponse->questionsamplesolutiontext = str_replace($name, "{$baseurl}/plots/{$file}", $renderresponse->questionsamplesolutiontext); + foreach ($renderresponse->questioninputs as $input) { + $input->samplesolutionrender = str_replace($name, "{$baseurl}/plots/{$file}", $input->samplesolutionrender); + } + } + $renderresponse->questionrender = $this->replace_feedback_tags($renderresponse->questionrender, $feedbackprefix); + $renderresponse->questionsamplesolutiontext = $this->replace_feedback_tags($renderresponse->questionsamplesolutiontext, $feedbackprefix); + } + $response->getBody()->write(json_encode($renderresponse)); return $response->withHeader('Content-Type', 'application/json'); } + + /** + * Replace [[feedback:????]] placeholder with an HTML div. + * + * @param string $text text to search for placeholders + * @param string $feedbackprefix prefix for feedback name attributes + * @return string + */ + public function replace_feedback_tags($text, $feedbackprefix) { + $result = $text; + preg_match_all('/\[\[feedback:([^\]]*)\]\]/', $text, $feedbacktags); + foreach ($feedbacktags[1] as $tag) { + $result = str_replace("[[feedback:{$tag}]]", "
", $result); + } + return $result; + } } diff --git a/api/public/stackjsvle.js b/api/public/stackjsvle.js index 5b09baa37e1..231411aab00 100644 --- a/api/public/stackjsvle.js +++ b/api/public/stackjsvle.js @@ -647,5 +647,13 @@ document.getElementById(targetdivid).replaceChildren(frm); IFRAMES[iframeid] = frm; + }; - }; \ No newline at end of file + /** + * Register an iframe if already created. + * @param string iframeid + */ + function register_iframe(iframeid) { + const iframe = document.getElementById(iframeid); + IFRAMES[iframeid] = iframe; + }; From 86c4b735f97894e4cd065e88af63131064fb16d4 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 13 Feb 2026 15:43:40 +0000 Subject: [PATCH 2/4] nrw3-2 - Unit test. --- api/README.md | 3 ++- api/controller/RenderController.php | 1 - tests/api_controller_test.php | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/api/README.md b/api/README.md index f944542edd0..dfdc8c5c25a 100644 --- a/api/README.md +++ b/api/README.md @@ -66,7 +66,8 @@ for all fields so minimum required XML is `register_iframe()` using the first array entry for each iframe in the response as the iframeid.) - `readOnly`: boolean. Determines whether rendered inputs are read only. The response is again a JSON document, with the following fields: diff --git a/api/controller/RenderController.php b/api/controller/RenderController.php index 03d5253c132..23eac2c22b2 100644 --- a/api/controller/RenderController.php +++ b/api/controller/RenderController.php @@ -38,7 +38,6 @@ use api\util\StackSeedHelper; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Slim\Http as Uri; // phpcs:ignore moodle.Commenting.MissingDocblock.Class class RenderController { diff --git a/tests/api_controller_test.php b/tests/api_controller_test.php index 2cd9422021e..28e1920c7fb 100644 --- a/tests/api_controller_test.php +++ b/tests/api_controller_test.php @@ -78,7 +78,7 @@ public function setUp(): void { $this->requestdata = []; $this->requestdata['seed'] = ''; $this->requestdata['readOnly'] = false; - $this->requestdata['renderInputs'] = true; + $this->requestdata['renderInputs'] = 'stackapi_input_'; // Need to mock request and response for the controllers but Moodle only // has the interfaces, not the classes themselves. We have to get an array @@ -175,6 +175,22 @@ public function test_render(): void { $this->assertEquals(false, $this->output->isinteractive); } + public function test_full_render(): void { + + $this->requestdata['fullRender'] = 'stackapi_val_,stackapi_fb_'; + $this->requestdata['questionDefinition'] = stack_api_test_data::get_question_string('iframes'); + $rc = new RenderController(); + $rc->__invoke($this->request, $this->response, []); + $this->assertEquals(1, count($this->output->iframes)); + $this->assertEquals(true, $this->output->isinteractive); + $this->assertStringContainsString("output->questionrender); + $this->assertStringContainsString("", $this->output->questionrender); + $this->assertStringContainsString("