Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
52 changes: 51 additions & 1 deletion api/controller/RenderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -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}]]", "<span name='{$validationprefix}{$tag}'></span>", $renderresponse->questionrender);
}
foreach ($renderresponse->iframes as $iframe) {
$iframe[1] = str_replace('<head>', "<head><base href=\"{$baseurl}\" />", $iframe[1]);
$renderediframe = "<iframe id=\"{$iframe[0]}\" style=\"width: 100%; height: 100%; border: 0;" . ($iframe[4] === 'false' ? ' overflow: hidden;' : '') . "\" scrolling=\"" . ($iframe[4] === 'false' ? 'no' : 'yes') . "\" title=\"{$iframe[4]}\" referrerpolicy=\"no-referrer\" " . (!$iframe[5] ? 'allow-scripts allow-downloads ' : '') . "srcdoc=\"" . htmlentities($iframe[1]) . "\"></iframe>";
$renderresponse->questionrender = str_replace("id=\"{$iframe[2]}\"></div>", "id=\"{$iframe[2]}\">{$renderediframe}</div>", $renderresponse->questionrender);
$renderresponse->questionsamplesolutiontext = str_replace("id=\"{$iframe[2]}\"></div>", "id=\"{$iframe[2]}\">{$renderediframe}</div>", $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}]]", "<div name='{$feedbackprefix}{$tag}'></div>", $result);
}
return $result;
}
}
10 changes: 9 additions & 1 deletion api/public/stackjsvle.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,5 +647,13 @@
document.getElementById(targetdivid).replaceChildren(frm);
IFRAMES[iframeid] = frm;

};

};
/**
* Register an iframe if already created.
* @param string iframeid
*/
function register_iframe(iframeid) {
const iframe = document.getElementById(iframeid);
IFRAMES[iframeid] = iframe;
};
18 changes: 17 additions & 1 deletion tests/api_controller_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("<input type=\"text\" name=\"stackapi_input_da_ans1\" ", $this->output->questionrender);
$this->assertStringContainsString("<span name='stackapi_val_da_ans1'></span>", $this->output->questionrender);
$this->assertStringContainsString("<iframe id=\"stack-iframe-1\" style=\"width: 100%; height: 100%; border: 0;\" " .
"scrolling=\"yes\" title=\"\" referrerpolicy=\"no-referrer\" allow-scripts allow-downloads srcdoc=",
$this->output->questionrender
);
}

public function test_render_specified_seed(): void {

$this->requestdata['seed'] = 219862533;
Expand Down
Loading