Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frage in iframe anzeigen lassen und JS-Unterstützung #153

Merged
merged 2 commits into from
Feb 13, 2025
Merged
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: 1 addition & 1 deletion amd/build/view_question.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion amd/build/view_question.min.js.map

Large diffs are not rendered by default.

155 changes: 152 additions & 3 deletions amd/src/view_question.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
import $ from "jquery";
import "theme_boost/bootstrap/popover";

/**
* @type {?Attempt} Attempt object that is passed to the question package.
*/
let attempt = null;

/**
* If the given input(-like) element is labelled, returns the label element. Returns null otherwise.
*
Expand Down Expand Up @@ -78,7 +83,8 @@ function markInvalid(element, message, ariaInvalid = true) {
$(popoverTarget).popover({
toggle: "popover",
trigger: "hover",
content: message
placement: "bottom",
content: message,
});
}

Expand Down Expand Up @@ -132,9 +138,15 @@ async function checkConstraints(element) {
}

/**
* Adds change event handlers for soft validation.
* Initializes the question.
*
* This function must be called within the iframe.
*
* @param {string} autoSaveHintInputId
* @param {string[]} roles QPy role names that the user has.
*/
export async function init() {
export async function init(autoSaveHintInputId, roles) {
// Add change event handlers for soft validation.
for (const element of document.querySelectorAll(`
[data-qpy_required], [data-qpy_pattern],
[data-qpy_minlength], [data-qpy_maxlength],
Expand All @@ -143,4 +155,141 @@ export async function init() {
await checkConstraints(element);
element.addEventListener("change", event => checkConstraints(event.target));
}

const form = window.document.getElementById("qpy-formulation");
if (form) {
// On form submit, submit the quiz's main form in the parent window instead.
form.addEventListener("submit", event => {
event.preventDefault();
window.frameElement.closest("form").submit();
});

// Modify a field in the main form in order to tell the Quiz's autosaver that the user changed an answer.
const autoSaveHintElement = parent.document.getElementById(autoSaveHintInputId);
if (autoSaveHintElement) {
form.addEventListener("change", function() {
autoSaveHintElement.value = parseInt(autoSaveHintElement.value) + 1;
});
}
}

// Attempt object that is passed to the question package.
attempt = new Attempt(
window.document.getElementById("qpy-formulation"),
window.document.getElementById("qpy-general-feedback"),
window.document.getElementById("qpy-specific-feedback"),
window.document.getElementById("qpy-right-answer"),
roles
);
}

/**
* Get a QuestionPy attempt.
*
* @returns {Attempt}
*/
export function getAttempt() {
if (attempt === null) {
throw new Error("Attempt not initialized");
}
return attempt;
}

class Attempt {
#formulation;
#generalFeedback;
#specificFeedback;
#rightAnswer;
#roles;

/**
* @param {Element} formulationElement
* @param {?Element} generalFeedbackElement
* @param {?Element} specificFeedbackElement
* @param {?Element} rightAnswer
* @param {string[]} roles
*/
constructor(formulationElement, generalFeedbackElement, specificFeedbackElement, rightAnswer, roles) {
this.#formulation = formulationElement;
this.#generalFeedback = generalFeedbackElement;
this.#specificFeedback = specificFeedbackElement;
this.#rightAnswer = rightAnswer;
this.#roles = roles;
}

/**
* Get the top html element where the question's formulation xhtml was inserted.
*
* @returns {Element}
*/
get formulationElement() {
return this.#formulation;
}

/**
* Get the top html element where the question's general feedback xhtml was inserted (if available).
*
* @returns {?Element}
*/
get generalFeedbackElement() {
return this.#generalFeedback;
}

/**
* Get the top html element where the question's specific feedback xhtml was inserted (if available).
*
* @returns {?Element}
*/
get specificFeedbackElement() {
return this.#specificFeedback;
}

/**
* Get the top html element where the question's right answer xhtml was inserted (if available).
*
* @returns {?Element}
*/
get rightAnswerElement() {
return this.#rightAnswer;
}

/**
* Get the names of the roles that the current user has.
*
* @typedef {'teacher' | 'developer' | 'scorer' | 'proctor'} roleName
* @returns {roleName[]}
*/
get userRoles() {
return this.#roles;
}
}

/**
* Add the question's form data located in the iframe to the main form when it is submitted.
*
* This function must be called outside the iframe, on the parent window.
*
* @param {string} iframeId - The ID of the question's iframe.
* @param {string} fieldPrefix - The prefix to add to the field names, for Moodle to recognize the fields belonging to a question.
* @return {void} This function does not return a value.
*/
export function addIframeFormDataOnSubmit(iframeId, fieldPrefix) {
const iframe = window.document.getElementById(iframeId);
if (iframe === null) {
window.console.error(`Could not find question iframe ${iframeId}. Cannot save answers.`);
return;
}

const form = iframe.closest("form");
form.addEventListener("formdata", event => {
const iframeForm = iframe.contentDocument.getElementById("qpy-formulation");
if (iframeForm === null) {
window.console.error("Could not find form in question iframe " + iframeId);
return;
}
const iframeFormData = new FormData(iframeForm);
for (const [key, value] of iframeFormData) {
event.formData.append(fieldPrefix + key, value);
}
});
}
12 changes: 11 additions & 1 deletion classes/api/attempt.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

namespace qtype_questionpy\api;

use qtype_questionpy\array_converter\attributes\array_element_class;
use qtype_questionpy\array_converter\attributes\array_key;

/**
* An attempt at a QuestionPy question.
*
Expand All @@ -31,14 +34,21 @@ class attempt {
/** @var attempt_ui */
public attempt_ui $ui;

/** @var package_dependency[] */
#[array_key('package_dependencies')]
#[array_element_class(package_dependency::class)]
public array $packagedependencies;

/**
* Initializes a new instance.
*
* @param int $variant
* @param attempt_ui $ui
* @param package_dependency[] $packagedependencies
*/
public function __construct(int $variant, attempt_ui $ui) {
public function __construct(int $variant, attempt_ui $ui, array $packagedependencies) {
$this->variant = $variant;
$this->ui = $ui;
$this->packagedependencies = $packagedependencies;
}
}
6 changes: 4 additions & 2 deletions classes/api/attempt_scored.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ class attempt_scored extends attempt {
* @param attempt_ui $ui
* @param scoring_code $scoringcode
* @param string|null $scoringstate
* @param package_dependency[] $packagedependencies
*/
public function __construct(int $variant, attempt_ui $ui, scoring_code $scoringcode, ?string $scoringstate = null) {
parent::__construct($variant, $ui);
public function __construct(int $variant, attempt_ui $ui, scoring_code $scoringcode, ?string $scoringstate,
array $packagedependencies) {
parent::__construct($variant, $ui, $packagedependencies);

$this->scoringstate = $scoringstate;
$this->scoringcode = $scoringcode;
Expand Down
5 changes: 3 additions & 2 deletions classes/api/attempt_started.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ class attempt_started extends attempt {
* @param int $variant
* @param attempt_ui $ui
* @param string $attemptstate
* @param array $packagedependencies
*/
public function __construct(int $variant, attempt_ui $ui, string $attemptstate) {
parent::__construct($variant, $ui);
public function __construct(int $variant, attempt_ui $ui, string $attemptstate, array $packagedependencies) {
parent::__construct($variant, $ui, $packagedependencies);
$this->attemptstate = $attemptstate;
}
}
5 changes: 5 additions & 0 deletions classes/api/attempt_ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class attempt_ui {
#[array_key('css_files')]
public ?array $cssfiles = null;

/** @var js_module_call[]|null */
#[array_key('javascript_calls')]
#[array_element_class(js_module_call::class)]
public array $javascriptcalls;

/** @var array<string, attempt_file> specifics TBD */
#[array_element_class(attempt_file::class)]
public array $files = [];
Expand Down
33 changes: 33 additions & 0 deletions classes/api/display_role.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace qtype_questionpy\api;

// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/**
* Possible display roles.
*
* @package qtype_questionpy
* @author Martin Gauk
* @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
enum display_role: string {
case developer = 'DEVELOPER';
case proctor = 'PROCTOR';
case scorer = 'SCORER';
case teacher = 'TEACHER';
}
33 changes: 33 additions & 0 deletions classes/api/feedback_type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace qtype_questionpy\api;

// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/**
* Possible feedback types.
*
* @package qtype_questionpy
* @author Martin Gauk
* @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
enum feedback_type: string {
case general_feedback = 'GENERAL_FEEDBACK';
case specific_feedback = 'SPECIFIC_FEEDBACK';
case right_answer = 'RIGHT_ANSWER';
case hint = 'HINT';
}
46 changes: 46 additions & 0 deletions classes/api/js_module_call.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace qtype_questionpy\api;

use qtype_questionpy\array_converter\attributes\array_key;

/**
* Model defining what JavaScript functions need to be called.
*
* @package qtype_questionpy
* @author Martin Gauk
* @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class js_module_call {
/** @var string */
public string $module;

/** @var string */
public string $function;

/** @var string|null */
public ?string $data;

/** @var display_role|null */
#[array_key('if_role')]
public ?display_role $ifrole;

/** @var feedback_type|null */
#[array_key('if_feedback_type')]
public ?feedback_type $iffeedbacktype;
}
Loading