first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,253 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Question behaviour where the student can submit questions one at a
* time for immediate feedback.
*
* @package qbehaviour
* @subpackage interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Question behaviour for the interactive model.
*
* Each question has a submit button next to it which the student can use to
* submit it. Once the question is submitted, it is not possible for the
* student to change their answer any more, but the student gets full feedback
* straight away.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactive extends question_behaviour_with_multiple_tries {
/**
* Constant used only in {@link adjust_display_options()} below and
* {@link (qbehaviour_interactive_renderer}.
* @var int
*/
const TRY_AGAIN_VISIBLE = 0x10;
/**
* Constant used only in {@link adjust_display_options()} below and
* {@link (qbehaviour_interactive_renderer}.
* @var int
*/
const TRY_AGAIN_VISIBLE_READONLY = 0x11;
public function is_compatible_question(question_definition $question) {
return $question instanceof question_automatically_gradable;
}
public function can_finish_during_attempt() {
return true;
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
/**
* @return bool are we are currently in the try_again state.
*/
public function is_try_again_state() {
$laststep = $this->qa->get_last_step();
return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') &&
$laststep->has_behaviour_var('_triesleft');
}
public function adjust_display_options(question_display_options $options) {
// We only need different behaviour in try again states.
if (!$this->is_try_again_state()) {
parent::adjust_display_options($options);
if ($this->qa->get_state() == question_state::$invalid &&
$options->marks == question_display_options::MARK_AND_MAX) {
$options->marks = question_display_options::MAX_ONLY;
}
return;
}
// The question in in a try-again state. We need the to let the renderer know this.
// The API for question-rendering is defined by the question engine, but we
// don't want to add logic in the renderer, so we are limited in how we can do this.
// However, when the question is in this state, all the question-type controls
// need to be rendered read-only. Therefore, we can conveniently pass this information
// by setting special true-like values in $options->readonly (but this is a bit of a hack).
$options->readonly = $options->readonly ? self::TRY_AGAIN_VISIBLE_READONLY : self::TRY_AGAIN_VISIBLE;
// Let the hint adjust the options.
$hint = $this->get_applicable_hint();
if (!is_null($hint)) {
$hint->adjust_display_options($options);
}
// Now call the base class method, but protect some fields from being overwritten.
$save = clone($options);
parent::adjust_display_options($options);
$options->feedback = $save->feedback;
$options->numpartscorrect = $save->numpartscorrect;
}
public function get_applicable_hint() {
if (!$this->is_try_again_state()) {
return null;
}
return $this->question->get_hint(count($this->question->hints) -
$this->qa->get_last_behaviour_var('_triesleft'), $this->qa);
}
public function get_expected_data() {
if ($this->is_try_again_state()) {
return array(
'tryagain' => PARAM_BOOL,
);
} else if ($this->qa->get_state()->is_active()) {
return array(
'submit' => PARAM_BOOL,
);
}
return parent::get_expected_data();
}
public function get_expected_qt_data() {
$hint = $this->get_applicable_hint();
if (!empty($hint->clearwrong)) {
return $this->question->get_expected_data();
}
return parent::get_expected_qt_data();
}
public function get_state_string($showcorrectness) {
$state = $this->qa->get_state();
if (!$state->is_active() || $state == question_state::$invalid) {
return parent::get_state_string($showcorrectness);
}
return get_string('triesremaining', 'qbehaviour_interactive',
$this->qa->get_last_behaviour_var('_triesleft'));
}
public function init_first_step(question_attempt_step $step, $variant) {
parent::init_first_step($step, $variant);
$step->set_behaviour_var('_triesleft', count($this->question->hints) + 1);
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
}
if ($this->is_try_again_state()) {
if ($pendingstep->has_behaviour_var('tryagain')) {
return $this->process_try_again($pendingstep);
} else {
return question_attempt::DISCARD;
}
} else {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('submit')) {
return $this->process_submit($pendingstep);
} else {
return $this->process_save($pendingstep);
}
}
}
public function summarise_action(question_attempt_step $step) {
if ($step->has_behaviour_var('comment')) {
return $this->summarise_manual_comment($step);
} else if ($step->has_behaviour_var('finish')) {
return $this->summarise_finish($step);
} else if ($step->has_behaviour_var('tryagain')) {
return get_string('tryagain', 'qbehaviour_interactive');
} else if ($step->has_behaviour_var('submit')) {
return $this->summarise_submit($step);
} else {
return $this->summarise_save($step);
}
}
public function process_try_again(question_attempt_pending_step $pendingstep) {
$pendingstep->set_state(question_state::$todo);
return question_attempt::KEEP;
}
public function process_submit(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
if (!$this->is_complete_response($pendingstep)) {
$pendingstep->set_state(question_state::$invalid);
} else {
$triesleft = $this->qa->get_last_behaviour_var('_triesleft');
$response = $pendingstep->get_qt_data();
list($fraction, $state) = $this->question->grade_response($response);
if ($state == question_state::$gradedright || $triesleft == 1) {
$pendingstep->set_state($state);
$pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
} else {
$pendingstep->set_behaviour_var('_triesleft', $triesleft - 1);
$pendingstep->set_state(question_state::$todo);
}
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
}
return question_attempt::KEEP;
}
protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
$totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
$triesleft = $this->qa->get_last_behaviour_var('_triesleft');
$fraction -= ($totaltries - $triesleft) * $this->question->penalty;
$fraction = max($fraction, 0);
return $fraction;
}
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
$response = $this->qa->get_last_qt_data();
if (!$this->question->is_gradable_response($response)) {
$pendingstep->set_state(question_state::$gaveup);
} else {
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
$pendingstep->set_state($state);
}
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
return question_attempt::KEEP;
}
public function process_save(question_attempt_pending_step $pendingstep) {
$status = parent::process_save($pendingstep);
if ($status == question_attempt::KEEP &&
$pendingstep->get_state() == question_state::$complete) {
$pendingstep->set_state(question_state::$todo);
}
return $status;
}
}
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Question behaviour type for interactive behaviour.
*
* @package qbehaviour_interactive
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Question behaviour type information for interactive behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactive_type extends question_behaviour_type {
public function is_archetypal() {
return true;
}
public function allows_multiple_submitted_responses() {
return true;
}
public function can_questions_finish_during_the_attempt() {
return true;
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Privacy Subsystem implementation for qbehaviour_interactive.
*
* @package qbehaviour_interactive
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_interactive\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_interactive implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Strings for component 'qbehaviour_interactive', language 'en'.
*
* @package qbehaviour
* @subpackage interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Interactive with multiple tries';
$string['privacy:metadata'] = 'The Interactive with multiple tries question behaviour plugin does not store any personal data.';
$string['triesremaining'] = 'Tries remaining: {$a}';
$string['tryagain'] = 'Try again';
@@ -0,0 +1,73 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Renderer for outputting parts of a question belonging to the interactive
* behaviour.
*
* @package qbehaviour
* @subpackage interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Interactive behaviour renderer.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactive_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
if ($options->readonly === qbehaviour_interactive::TRY_AGAIN_VISIBLE ||
$options->readonly === qbehaviour_interactive::TRY_AGAIN_VISIBLE_READONLY) {
// We are in the try again state, so no submit button.
return '';
}
return $this->submit_button($qa, $options);
}
public function feedback(question_attempt $qa, question_display_options $options) {
// Show the Try again button if we are in try-again state.
if (!$qa->get_state()->is_active() ||
($options->readonly !== qbehaviour_interactive::TRY_AGAIN_VISIBLE &&
$options->readonly !== qbehaviour_interactive::TRY_AGAIN_VISIBLE_READONLY)) {
return '';
}
$attributes = [
'type' => 'submit',
'id' => $qa->get_behaviour_field_name('tryagain'),
'name' => $qa->get_behaviour_field_name('tryagain'),
'value' => get_string('tryagain', 'qbehaviour_interactive'),
'class' => 'submit btn btn-secondary',
'data-savescrollposition' => 'true',
];
if ($options->readonly === qbehaviour_interactive::TRY_AGAIN_VISIBLE_READONLY) {
// This means the question really was rendered with read-only option.
$attributes['disabled'] = 'disabled';
}
$output = html_writer::empty_tag('input', $attributes);
if (empty($attributes['disabled'])) {
$this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]);
}
return $output;
}
}
@@ -0,0 +1,63 @@
<?php
// This file is part of Moodle - http://moodle.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 qbehaviour_interactive;
use question_engine;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
require_once(__DIR__ . '/../../../engine/tests/helpers.php');
/**
* Unit tests for the interactive with multiple tries behaviour type class.
*
* @package qbehaviour_interactive
* @category test
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behaviour_type_test extends \basic_testcase {
/** @var qbehaviour_interactive_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('interactive');
}
public function test_is_archetypal(): void {
$this->assertTrue($this->behaviourtype->is_archetypal());
}
public function test_get_unused_display_options(): void {
$this->assertEquals(array(),
$this->behaviourtype->get_unused_display_options());
}
public function test_can_questions_finish_during_the_attempt(): void {
$this->assertTrue($this->behaviourtype->can_questions_finish_during_the_attempt());
}
public function test_adjust_random_guess_score(): void {
$this->assertEquals(0, $this->behaviourtype->adjust_random_guess_score(0));
$this->assertEquals(1, $this->behaviourtype->adjust_random_guess_score(1));
}
}
@@ -0,0 +1,533 @@
<?php
// This file is part of Moodle - http://moodle.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 qbehaviour_interactive;
use question_display_options;
use question_hint;
use question_hint_with_parts;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
require_once(__DIR__ . '/../../../engine/tests/helpers.php');
/**
* Unit tests for the interactive behaviour.
*
* @package qbehaviour_interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class walkthrough_test extends \qbehaviour_walkthrough_test_base {
public function test_interactive_feedback_multichoice_right(): void {
// Create a multichoice single question.
$mc = \test_question_maker::make_a_multichoice_single_question();
$mc->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
);
$this->start_attempt_at_question($mc, 'interactive');
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_question_text_expectation($mc),
$this->get_contains_mc_radio_expectation(0, true, false),
$this->get_contains_mc_radio_expectation(1, true, false),
$this->get_contains_mc_radio_expectation(2, true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Save the wrong answer.
$this->process_submission(array('answer' => $wrongindex));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation());
// Submit the wrong answer.
$this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new \question_pattern_expectation('/Tries remaining: 2/'),
$this->get_contains_hint_expectation('This is the first hint'));
// Check that, if we review in this state, the try again button is disabled.
$displayoptions = new question_display_options();
$displayoptions->readonly = true;
$html = $this->quba->render_question($this->slot, $displayoptions);
$this->assert($this->get_contains_try_again_button_expectation(false), $html);
// Do try again.
$this->process_submission(array('-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
// Submit the right answer.
$this->process_submission(array('answer' => $rightindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(0.6666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.6666667),
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
// Finish the attempt - should not need to add a new state.
$numsteps = $this->get_step_count();
$this->quba->finish_all_questions();
// Verify.
$this->assertEquals($numsteps, $this->get_step_count());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(0.6666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.6666667),
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
$this->get_contains_correct_expectation(),
$this->get_no_hint_visible_expectation());
// Process a manual comment.
$this->manual_grade('Not good enough!', 0.5, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_mark_summary(0.5),
$this->get_contains_partcorrect_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Check regrading does not mess anything up.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_mark_summary(0.5),
$this->get_contains_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), 0.6666667, 0.0000001);
}
public function test_interactive_finish_when_try_again_showing(): void {
// Create a multichoice single question.
$mc = \test_question_maker::make_a_multichoice_single_question();
$mc->showstandardinstruction = true;
$mc->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
);
$this->start_attempt_at_question($mc, 'interactive');
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_question_text_expectation($mc),
$this->get_contains_mc_radio_expectation(0, true, false),
$this->get_contains_mc_radio_expectation(1, true, false),
$this->get_contains_mc_radio_expectation(2, true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation(),
new \question_pattern_expectation('/' .
preg_quote(get_string('selectone', 'qtype_multichoice'), '/') . '/'));
// Submit the wrong answer.
$this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new \question_pattern_expectation('/Tries remaining: 1/'),
$this->get_contains_hint_expectation('This is the first hint'));
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
$this->get_contains_incorrect_expectation(),
$this->get_no_hint_visible_expectation());
}
public function test_interactive_shortanswer_try_to_submit_blank(): void {
// Create a short answer question.
$sa = \test_question_maker::make_question('shortanswer');
$sa->hints = array(
new question_hint(0, 'This is the first hint.', FORMAT_HTML),
new question_hint(0, 'This is the second hint.', FORMAT_HTML),
);
$this->start_attempt_at_question($sa, 'interactive');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_no_hint_visible_expectation());
// Submit blank.
$this->process_submission(array('-submit' => 1, 'answer' => ''));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_no_hint_visible_expectation());
// Now get it wrong.
$this->process_submission(array('-submit' => 1, 'answer' => 'newt'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_contains_try_again_button_expectation(true),
new \question_pattern_expectation('/Tries remaining: 2/'),
$this->get_contains_hint_expectation('This is the first hint'));
$this->assertEquals('newt',
$this->quba->get_response_summary($this->slot));
// Try again.
$this->process_submission(array('-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_no_hint_visible_expectation());
// Now submit blank again.
$this->process_submission(array('-submit' => 1, 'answer' => ''));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_no_hint_visible_expectation());
// Now get it right.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(0.6666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.6666667),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_no_hint_visible_expectation());
$this->assertEquals('frog',
$this->quba->get_response_summary($this->slot));
}
public function test_interactive_feedback_multichoice_multiple_reset(): void {
// Create a multichoice multiple question.
$mc = \test_question_maker::make_a_multichoice_multi_question();
$mc->showstandardinstruction = true;
$mc->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
);
$this->start_attempt_at_question($mc, 'interactive', 2);
$right = array_keys($mc->get_correct_response());
$wrong = array_diff(array('choice0', 'choice1', 'choice2', 'choice3'), $right);
$wrong = array_values(array_diff(
array('choice0', 'choice1', 'choice2', 'choice3'), $right));
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_question_text_expectation($mc),
$this->get_contains_mc_checkbox_expectation('choice0', true, false),
$this->get_contains_mc_checkbox_expectation('choice1', true, false),
$this->get_contains_mc_checkbox_expectation('choice2', true, false),
$this->get_contains_mc_checkbox_expectation('choice3', true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_num_parts_correct(),
$this->get_tries_remaining_expectation(3),
$this->get_no_hint_visible_expectation(),
new \question_pattern_expectation('/' .
preg_quote(get_string('selectmulti', 'qtype_multichoice'), '/') . '/'));
// Submit an answer with one right, and one wrong.
$this->process_submission(array($right[0] => 1, $wrong[0] => 1, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_checkbox_expectation($right[0], false, true),
$this->get_contains_mc_checkbox_expectation($right[1], false, false),
$this->get_contains_mc_checkbox_expectation($wrong[0], false, true),
$this->get_contains_mc_checkbox_expectation($wrong[1], false, false),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new \question_pattern_expectation('/Tries remaining: 2/'),
$this->get_contains_hint_expectation('This is the first hint'),
$this->get_contains_num_parts_correct(1),
$this->get_contains_standard_incorrect_combined_feedback_expectation(),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . $right[0], '1'),
$this->get_does_not_contain_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . $right[1]),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . $wrong[0], '0'),
$this->get_does_not_contain_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . $wrong[1]));
// Do try again.
$this->process_submission(array($right[0] => 1, '-tryagain' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_mc_checkbox_expectation($right[0], true, true),
$this->get_contains_mc_checkbox_expectation($right[1], true, false),
$this->get_contains_mc_checkbox_expectation($wrong[0], true, false),
$this->get_contains_mc_checkbox_expectation($wrong[1], true, false),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
}
public function test_interactive_regrade_changing_num_tries_leaving_open(): void {
// Create a multichoice multiple question.
$q = \test_question_maker::make_question('shortanswer');
$q->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
);
$this->start_attempt_at_question($q, 'interactive', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_tries_remaining_expectation(3));
// Submit the right answer.
$this->process_submission(array('answer' => 'frog', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
// Now change the quiestion so that answer is only partially right, and regrade.
$q->answers[13]->fraction = 0.6666667;
$q->answers[14]->fraction = 1;
$this->quba->regrade_all_questions(false);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
}
public function test_interactive_regrade_changing_num_tries_finished(): void {
// Create a multichoice multiple question.
$q = \test_question_maker::make_question('shortanswer');
$q->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
);
$this->start_attempt_at_question($q, 'interactive', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_tries_remaining_expectation(3));
// Submit the right answer.
$this->process_submission(array('answer' => 'frog', '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
// Now change the quiestion so that answer is only partially right, and regrade.
$q->answers[13]->fraction = 0.6666667;
$q->answers[14]->fraction = 1;
$this->quba->regrade_all_questions(true);
// Verify.
$this->check_current_state(question_state::$gradedpartial);
// TODO I don't think 1 is the right fraction here. However, it is what
// you get attempting a question like this without regrading being involved,
// and I am currently interested in testing regrading here.
$this->check_current_mark(1);
}
public function test_review_of_interactive_questions_before_finished(): void {
// Create a multichoice multiple question.
$q = \test_question_maker::make_question('shortanswer');
$q->hints = array(
new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
);
$this->start_attempt_at_question($q, 'interactive', 3);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_does_not_contain_try_again_button_expectation());
// Now check what the teacher sees when they review the question.
$this->displayoptions->readonly = true;
$this->check_current_output(
$this->get_contains_submit_button_expectation(false),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
$this->get_does_not_contain_try_again_button_expectation());
$this->displayoptions->readonly = false;
// Submit a wrong answer.
$this->process_submission(array('answer' => 'cat', '-submit' => 1));
// Check the Try again button now shows up correctly.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_hint_expectation('This is the first hint.'),
$this->get_tries_remaining_expectation(2),
$this->get_contains_try_again_button_expectation(true));
// And check that a disabled Try again button shows up when the question is reviewed.
$this->displayoptions->readonly = true;
$this->check_current_output(
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_hint_expectation('This is the first hint.'),
$this->get_tries_remaining_expectation(2),
$this->get_contains_try_again_button_expectation(false));
}
}
@@ -0,0 +1,33 @@
<?php
// This file is part of Moodle - http://moodle.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/>.
/**
* Version information for the calculated question type.
*
* @package qbehaviour
* @subpackage interactive
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbehaviour_interactive';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;