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
3 changes: 3 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 All @@ -79,6 +81,7 @@ The response is again a JSON document, with the following fields:
- an int array `questionvariants` containing all variant seeds of the question
- an array of arrays `iframes` of arguments to create iframes to hold JS panels e.g. JSXGraph, GeoGebra
- a boolean field `isinteractive`, indicating if the question contains elements preventing a static representation. If true, a printed version of the question would make no sense
- a string field `questionnote`, containing the rendered questionnote of the question

The input configuration consists of the following fields:

Expand Down
58 changes: 54 additions & 4 deletions 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 @@ -134,15 +133,66 @@ public function __invoke(Request $request, Response $response, array $args): Res

// Necessary, as php will otherwise encode this as an empty array, instead of an empty object.
$renderresponse->questioninputs = (object) $inputs;

$renderresponse->questionassets = (object) $plots;

$renderresponse->questionseed = $question->seed;
$renderresponse->questionvariants = $question->deployedseeds;
$renderresponse->iframes = StackIframeHolder::$iframes;
$renderresponse->isinteractive = $question->is_interactive();
$renderresponse->questionnote = $question->get_question_summary();
StackPlotReplacer::replace_plots($plots, $renderresponse->questionnote, "note-" . $name, $storeprefix);
$renderresponse->questionassets = (object) $plots;

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;
}
}
2 changes: 2 additions & 0 deletions api/dtos/StackRenderResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class StackRenderResponse {
public $iframes;
/** @var bool */
public $isinteractive;
/** @var string */
public $questionnote;
}

// phpcs:ignore moodle.Commenting.MissingDocblock.Class
Expand Down
4 changes: 4 additions & 0 deletions api/public/stack.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ function render_directory($dirdetails) {
<h2>General feedback:</h2>
<div id="generalfeedback" class="feedback"></div>
</div>
<div id="stackapi_questionnote" class="col-lg-8" style="display: none">
<h2>Question note:</h2>
<div id="questionnote" class="feedback"></div>
</div>
<h2 id="stackapi_score" style="display: none">Score: <span id="score"></span></h2>
<div id="stackapi_summary" class="col-lg-10" style="display: none">
<h2><?php echo stack_string('api_response')?>:</h2>
Expand Down
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;
};
3 changes: 3 additions & 0 deletions api/public/stackshared.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ function send() {
for (const [name, file] of Object.entries(json.questionassets)) {
question = question.replace(name, `${serverUrl}plots/${file}`);
json.questionsamplesolutiontext = json.questionsamplesolutiontext.replace(name, `${serverUrl}plots/${file}`);
json.questionnote = json.questionnote.replace(name, `${serverUrl}plots/${file}`);
correctAnswers = correctAnswers.replace(name, `${serverUrl}plots/${file}`);
}
question = replaceFeedbackTags(question);
Expand Down Expand Up @@ -171,6 +172,8 @@ function send() {
// If the question is updated, there may no longer be general feedback.
document.getElementById('stackapi_generalfeedback').style.display = 'none';
}
document.getElementById('stackapi_questionnote').style.display = 'block';
document.getElementById('questionnote').innerHTML = json.questionnote;
document.getElementById('stackapi_score').style.display = 'none';
} else {
if (sampleText) {
Expand Down
30 changes: 28 additions & 2 deletions 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 @@ -173,6 +173,27 @@ public function test_render(): void {
$this->assertEquals(3, count($this->output->questionvariants));
$this->assertEquals(0, count($this->output->iframes));
$this->assertEquals(false, $this->output->isinteractive);
$this->assertEquals(
"\\({\\left[\\begin{array}{cc} 5 & 5 \\\\ 4 & 4 \\end{array}\\right]}.{\\left[\\begin{array}{cc} " .
"2 & 4 \\\\ 5 & 2 \\end{array}\\right]}={\\left[\\begin{array}{cc} 35 & 30 \\\\ 28 & 24 \\end{array}\\right]}\\)",
$this->output->questionnote
);
}

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 {
Expand All @@ -190,11 +211,16 @@ public function test_render_plots(): void {
$this->requestdata['questionDefinition'] = stack_api_test_data::get_question_string('plots');
$rc = new RenderController();
$rc->__invoke($this->request, $this->response, []);
$this->assertEquals(4, count((array)$this->output->questionassets));
$this->assertEquals(9, count((array)$this->output->questionassets));
$this->assertEquals(true, isset($this->output->questionassets->{'input-ans1-1-0.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'input-ans1-2-0.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'input-ans1-3-0.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'input-ans1-4-0.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'note-ans1-0.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'note-ans1-1.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'note-ans1-2.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'note-ans1-3.svg'}));
$this->assertEquals(true, isset($this->output->questionassets->{'note-ans1-4.svg'}));
$this->assertEquals(false, $this->output->isinteractive);
}

Expand Down
Loading