diff --git a/api/README.md b/api/README.md index f944542edd0..d58cf94c835 100644 --- a/api/README.md +++ b/api/README.md @@ -67,6 +67,8 @@ can also be used. (See [Diff Route](#diff-route).) Any non-empty YAML will do e. - `seed`: Seed to choose a question variant. Must be contained in the list of deployed variants. If no seed is provided, the first deployed variant is used. - `renderInputs`: String. Response will include HTML renders of the inputs if value other than ''. The input divs will have the value added as a prefix to their name attribute. +- `fullRender`: Array consisting of a string prefix for validation divs and a string prefix for feedback divs e.g. `['validationprefix','feedbackprefix']` (`renderInputs` must also be set.) Response `questionrender` and `questionsamplesolutiontext` will be the full HTML render of the question with the inputs inserted in the correct place, full plot URLs, placeholders replaced with HTML and iframes included. Iframes will still need to be registered on the front +end to be displayed properly. (`stackjsvle.js->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 fcef706e31b..5e196045ae4 100644 --- a/api/controller/RenderController.php +++ b/api/controller/RenderController.php @@ -45,7 +45,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 +141,58 @@ public function __invoke(Request $request, Response $response, array $args): Res $renderresponse->iframes = StackIframeHolder::$iframes; $renderresponse->isinteractive = $question->is_interactive(); + if (!empty($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] = $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; + }; diff --git a/tests/api_controller_test.php b/tests/api_controller_test.php index 2cd9422021e..991bde53bb1 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("