diff --git a/.gitignore b/.gitignore index a67a00f6..b8687642 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # Since package.json only contains dev dependencies, comitting the lockfile makes little sence. /package-lock.json /node_modules + +.idea diff --git a/classes/question_service.php b/classes/question_service.php index 0c2008cb..2789c5ca 100644 --- a/classes/question_service.php +++ b/classes/question_service.php @@ -70,6 +70,7 @@ public function get_question(int $questionid): object { $result->qpy_package_hash = $package->hash; $result->qpy_state = $record->state; + $result->shuffleanswers = $record->shuffleanswers; } return $result; @@ -124,6 +125,10 @@ public function upsert_question(object $question): void { $update["pkgversionid"] = $pkgversionid; } + if ($question->shuffleanswers !== $existingrecord->shuffleanswers) { + $update["shuffleanswers"] = $question->shuffleanswers; + } + if (count($update) > 1) { $DB->update_record(self::QUESTION_TABLE, (object)$update); } @@ -134,6 +139,7 @@ public function upsert_question(object $question): void { "feedback" => "", "pkgversionid" => $pkgversionid, "state" => $response->state, + "shuffleanswers" => $question->shuffleanswers, ]); } } diff --git a/classes/question_ui_renderer.php b/classes/question_ui_renderer.php index a2237f60..ddf56361 100644 --- a/classes/question_ui_renderer.php +++ b/classes/question_ui_renderer.php @@ -53,6 +53,9 @@ class question_ui_renderer { /** @var question_metadata|null $metadata */ private ?question_metadata $metadata = null; + /** @var boolean Whether the questions answers should be shuffled. */ + public bool $shuffleanswers; + /** * Parses the given XML and initializes a new {@see question_ui_renderer} instance. * @@ -282,14 +285,16 @@ private function shuffle_contents(\DOMXPath $xpath): void { $childelements[] = $child; } } - shuffle($childelements); + if ($this->shuffleanswers) { + shuffle($childelements); + } // Iterate over children, replacing elements with random ones while copying everything else. $i = 1; while ($element->hasChildNodes()) { $child = $element->firstChild; if ($child instanceof DOMElement) { - $child = array_pop($childelements); + $child = array_shift($childelements); $newelement->appendChild($child); $this->replace_shuffled_indices($xpath, $child, $i++); } else { diff --git a/db/install.xml b/db/install.xml index 8b5ddc0b..e3b25bb1 100644 --- a/db/install.xml +++ b/db/install.xml @@ -28,6 +28,7 @@ + diff --git a/edit_questionpy_form.php b/edit_questionpy_form.php index a53560e8..fb960e56 100644 --- a/edit_questionpy_form.php +++ b/edit_questionpy_form.php @@ -129,6 +129,10 @@ protected function definition_inner($mform): void { $mform->addElement('hidden', 'qpy_package_hash', ''); $mform->setType('qpy_package_hash', PARAM_RAW); + // Whether the content of a qpy:shuffle-contents can be randomly shuffled. + $mform->addElement('hidden', 'shuffleanswers', get_config('qtype_questionpy', 'shuffleanswers')); + $mform->setType('shuffleanswers', PARAM_BOOL); + // While not a button, we need a way of telling moodle not to save the submitted data to the question when the // package has simply been changed. The hidden element is enabled from JS when a package is selected or changed. $mform->registerNoSubmitButton('qpy_package_changed'); diff --git a/lang/en/qtype_questionpy.php b/lang/en/qtype_questionpy.php index 05651cef..c4edf883 100644 --- a/lang/en/qtype_questionpy.php +++ b/lang/en/qtype_questionpy.php @@ -34,6 +34,7 @@ $string['server_password_description'] = 'The Password to access the Application Server'; $string['server_timeout'] = 'Server timeout time'; $string['server_timeout_description'] = 'Server timeout time in seconds'; +$string['heading_packages'] = 'Packages'; $string['max_package_size_kb'] = 'Maximum file size of a QuestionPy package'; $string['max_package_size_kb_description'] = 'Maximum file size in kB'; $string['packages_subheading'] = 'Packages'; @@ -52,6 +53,10 @@ $string['server_info_requests_in_process'] = 'Requests in process'; $string['server_info_requests_in_queue'] = 'Requests in queue'; +// Question settings. +$string['shuffleanswers'] = 'Shuffle the contents?'; +$string['shuffleanswers_desc'] = 'Whether the content of a qpy:shuffle-contents should be randomly shuffled for each attempt by default.'; + // Package upload. $string['formerror_noqpy_package'] = 'Selected file must be of type .qpy'; diff --git a/question.php b/question.php index 5f629397..655feb7f 100644 --- a/question.php +++ b/question.php @@ -45,6 +45,8 @@ class qtype_questionpy_question extends question_graded_automatically_with_count private string $packagehash; /** @var string */ private string $questionstate; + /** @var boolean Whether the questions answers should be shuffled. */ + public bool $shuffleanswers; // Properties which do change between attempts (i.e. are modified by start_attempt and apply_attempt_state). /** @var string */ @@ -92,6 +94,7 @@ public function start_attempt(question_attempt_step $step, $variant): void { $this->scoringstate = null; $this->ui = new question_ui_renderer($attempt->ui->content, $attempt->ui->placeholders); + $this->ui->shuffleanswers = $this->shuffleanswers; } /** @@ -123,6 +126,7 @@ public function apply_attempt_state(question_attempt_step $step) { $attempt = $this->api->view_attempt($this->packagehash, $this->questionstate, $this->attemptstate, $this->scoringstate); $this->ui = new question_ui_renderer($attempt->ui->content, $attempt->ui->placeholders); + $this->ui->shuffleanswers = $this->shuffleanswers; } /** @@ -223,6 +227,7 @@ public function grade_response(array $response): array { $response ); $this->ui = new question_ui_renderer($attemptscored->ui->content, $attemptscored->ui->placeholders); + $this->ui->shuffleanswers = $this->shuffleanswers; // TODO: Persist scoring state. We need to set a qtvar, but we don't have access to the pending step here. $this->scoringstate = $attemptscored->scoringstate; switch ($attemptscored->scoringcode) { diff --git a/questiontype.php b/questiontype.php index 401d7f62..79290b79 100644 --- a/questiontype.php +++ b/questiontype.php @@ -145,4 +145,14 @@ public function get_question_options($question): bool { protected function make_question_instance($questiondata) { return new qtype_questionpy_question($questiondata->qpy_package_hash, $questiondata->qpy_state); } + + /** + * Initialise the question_definition fields. + * @param question_definition $question the question_definition we are creating. + * @param object $questiondata the question data loaded from the database. + */ + protected function initialise_question_instance(question_definition $question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + $question->shuffleanswers = $questiondata->shuffleanswers; + } } diff --git a/renderer.php b/renderer.php index 4e2afc6a..1cef094c 100644 --- a/renderer.php +++ b/renderer.php @@ -53,8 +53,19 @@ public function head_code(question_attempt $qa) { * @throws coding_exception */ public function formulation_and_controls(question_attempt $qa, question_display_options $options): string { + global $DB; $question = $qa->get_question(); assert($question instanceof qtype_questionpy_question); + + // Check if we are in a quiz context. + if ($this->page->context->contextlevel == CONTEXT_MODULE && $this->page->cm->modname == 'quiz') { + $quiz = $DB->get_record('quiz', ['id' => $this->page->cm->instance]); + if ($quiz) { + // Access the shuffleanswers property of the quiz. + $question->ui->shuffleanswers = $quiz->shuffleanswers; + } + } + return $question->ui->render_formulation($qa, $options); } diff --git a/settings.php b/settings.php index dc62fb4c..bf44abe1 100644 --- a/settings.php +++ b/settings.php @@ -56,7 +56,7 @@ // Package settings. $settings->add(new admin_setting_heading( 'qtype_questionpy/heading_packages', - 'Packages', + new lang_string('heading_packages', 'qtype_questionpy'), null )); @@ -71,6 +71,12 @@ 5 )); + // Question settings. + $settings->add(new admin_setting_configcheckbox('qtype_questionpy/shuffleanswers', + new lang_string('shuffleanswers', 'qtype_questionpy'), + new lang_string('shuffleanswers_desc', 'qtype_questionpy'), '1')); + + // Server Status/Info. $settings->add(new admin_setting_heading( 'qtype_questionpy/server_info', new lang_string('server_info_heading', 'qtype_questionpy'),