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
+340
View File
@@ -0,0 +1,340 @@
<?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 for the old adaptive mode.
*
* @package qbehaviour
* @subpackage adaptive
* @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 adaptive mode.
*
* This is the old version of interactive mode.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptive extends question_behaviour_with_multiple_tries {
const IS_ARCHETYPAL = true;
public function is_compatible_question(question_definition $question) {
return $question instanceof question_automatically_gradable;
}
public function get_expected_data() {
if ($this->qa->get_state()->is_active()) {
return array('submit' => PARAM_BOOL);
}
return parent::get_expected_data();
}
public function get_state_string($showcorrectness) {
$laststep = $this->qa->get_last_step();
if ($laststep->has_behaviour_var('_try')) {
$state = question_state::graded_state_for_fraction(
$laststep->get_behaviour_var('_rawfraction'));
return $state->default_string(true);
}
$state = $this->qa->get_state();
if ($state == question_state::$todo) {
return get_string('notcomplete', 'qbehaviour_adaptive');
} else {
return parent::get_state_string($showcorrectness);
}
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
public function adjust_display_options(question_display_options $options) {
// Save some bits so we can put them back later.
$save = clone($options);
// Do the default thing.
parent::adjust_display_options($options);
// Then, if they have just Checked an answer, show them the applicable bits of feedback.
if (!$this->qa->get_state()->is_finished() &&
$this->qa->get_last_behaviour_var('_try')) {
$options->feedback = $save->feedback;
$options->correctness = $save->correctness;
$options->numpartscorrect = $save->numpartscorrect;
}
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($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('submit')) {
return $this->summarise_submit($step);
} else {
return $this->summarise_save($step);
}
}
public function process_save(question_attempt_pending_step $pendingstep) {
$status = parent::process_save($pendingstep);
$prevgrade = $this->qa->get_fraction();
if (!is_null($prevgrade)) {
$pendingstep->set_fraction($prevgrade);
}
$pendingstep->set_state(question_state::$todo);
return $status;
}
protected function adjusted_fraction($fraction, $prevtries) {
return $fraction - $this->question->penalty * $prevtries;
}
public function process_submit(question_attempt_pending_step $pendingstep) {
$status = $this->process_save($pendingstep);
$response = $pendingstep->get_qt_data();
if (!$this->question->is_complete_response($response)) {
$pendingstep->set_state(question_state::$invalid);
if ($this->qa->get_state() != question_state::$invalid) {
$status = question_attempt::KEEP;
}
return $status;
}
$prevstep = $this->qa->get_last_step_with_behaviour_var('_try');
$prevresponse = $prevstep->get_qt_data();
$prevtries = $this->qa->get_last_behaviour_var('_try', 0);
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
if ($this->question->is_same_response($response, $prevresponse)) {
return question_attempt::DISCARD;
}
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
if ($prevstep->get_state() == question_state::$complete) {
$pendingstep->set_state(question_state::$complete);
} else if ($state == question_state::$gradedright) {
$pendingstep->set_state(question_state::$complete);
} else {
$pendingstep->set_state(question_state::$todo);
}
$pendingstep->set_behaviour_var('_try', $prevtries + 1);
$pendingstep->set_behaviour_var('_rawfraction', $fraction);
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
return question_attempt::KEEP;
}
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
$prevtries = $this->qa->get_last_behaviour_var('_try', 0);
$prevbest = $this->qa->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
$laststep = $this->qa->get_last_step();
$response = $laststep->get_qt_data();
if (!$this->question->is_gradable_response($response)) {
$state = question_state::$gaveup;
$fraction = 0;
} else {
if ($laststep->has_behaviour_var('_try')) {
// Last answer was graded, we want to regrade it. Otherwise the answer
// has changed, and we are grading a new try.
$prevtries -= 1;
}
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_behaviour_var('_try', $prevtries + 1);
$pendingstep->set_behaviour_var('_rawfraction', $fraction);
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
}
$pendingstep->set_state($state);
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
return question_attempt::KEEP;
}
/**
* Got the most recently graded step. This is mainly intended for use by the
* renderer.
* @return question_attempt_step the most recently graded step.
*/
public function get_graded_step() {
$step = $this->qa->get_last_step_with_behaviour_var('_try');
if ($step->has_behaviour_var('_try')) {
return $step;
} else {
return null;
}
}
/**
* Determine whether a question state represents an "improvable" result,
* that is, whether the user can still improve their score.
*
* @param question_state $state the question state.
* @return bool whether the state is improvable
*/
public function is_state_improvable(question_state $state) {
return $state == question_state::$todo;
}
/**
* @return qbehaviour_adaptive_mark_details the information about the current state-of-play, scoring-wise,
* for this adaptive attempt.
*/
public function get_adaptive_marks() {
// Try to find the last graded step.
$gradedstep = $this->get_graded_step();
if (is_null($gradedstep) || $this->qa->get_max_mark() == 0) {
// No score yet.
return new qbehaviour_adaptive_mark_details(question_state::$todo);
}
// Work out the applicable state.
if ($this->qa->get_state()->is_commented()) {
$state = $this->qa->get_state();
} else {
$state = question_state::graded_state_for_fraction(
$gradedstep->get_behaviour_var('_rawfraction'));
}
// Prepare the grading details.
$details = $this->adaptive_mark_details_from_step($gradedstep, $state, $this->qa->get_max_mark(), $this->question->penalty);
$details->improvable = $this->is_state_improvable($this->qa->get_state());
return $details;
}
/**
* Actually populate the qbehaviour_adaptive_mark_details object.
* @param question_attempt_step $gradedstep the step that holds the relevant mark details.
* @param question_state $state the state corresponding to $gradedstep.
* @param unknown_type $maxmark the maximum mark for this question_attempt.
* @param unknown_type $penalty the penalty for this question, as a fraction.
*/
protected function adaptive_mark_details_from_step(question_attempt_step $gradedstep,
question_state $state, $maxmark, $penalty) {
$details = new qbehaviour_adaptive_mark_details($state);
$details->maxmark = $maxmark;
$details->actualmark = $gradedstep->get_fraction() * $details->maxmark;
$details->rawmark = $gradedstep->get_behaviour_var('_rawfraction') * $details->maxmark;
$details->currentpenalty = $penalty * $details->maxmark;
$details->totalpenalty = $details->currentpenalty * $this->qa->get_last_behaviour_var('_try', 0);
$details->improvable = $this->is_state_improvable($gradedstep->get_state());
return $details;
}
}
/**
* This class encapsulates all the information about the current state-of-play
* scoring-wise. It is used to communicate between the beahviour and the renderer.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptive_mark_details {
/** @var question_state the current state of the question. */
public $state;
/** @var float the maximum mark for this question. */
public $maxmark;
/** @var float the current mark for this question. */
public $actualmark;
/** @var float the raw mark for this question before penalties were applied. */
public $rawmark;
/** @var float the the amount of additional penalty this attempt attracted. */
public $currentpenalty;
/** @var float the total that will apply to future attempts. */
public $totalpenalty;
/** @var bool whether it is possible for this mark to be improved in future. */
public $improvable;
/**
* Constructor.
* @param question_state $state
*/
public function __construct($state, $maxmark = null, $actualmark = null, $rawmark = null,
$currentpenalty = null, $totalpenalty = null, $improvable = null) {
$this->state = $state;
$this->maxmark = $maxmark;
$this->actualmark = $actualmark;
$this->rawmark = $rawmark;
$this->currentpenalty = $currentpenalty;
$this->totalpenalty = $totalpenalty;
$this->improvable = $improvable;
}
/**
* Get the marks, formatted to a certain number of decimal places, in the
* form required by calls like get_string('gradingdetails', 'qbehaviour_adaptive', $a).
* @param int $markdp the number of decimal places required.
* @return array ready to substitute into language strings.
*/
public function get_formatted_marks($markdp) {
return array(
'max' => format_float($this->maxmark, $markdp),
'cur' => format_float($this->actualmark, $markdp),
'raw' => format_float($this->rawmark, $markdp),
'penalty' => format_float($this->currentpenalty, $markdp),
'totalpenalty' => format_float($this->totalpenalty, $markdp),
);
}
}
@@ -0,0 +1,43 @@
<?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 adaptive behaviour.
*
* @package qbehaviour_adaptive
* @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 adaptive behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptive_type extends question_behaviour_type {
public function is_archetypal() {
return true;
}
public function allows_multiple_submitted_responses() {
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_adaptive.
*
* @package qbehaviour_adaptive
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_adaptive\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_adaptive 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,41 @@
<?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_adaptive', language 'en'.
*
* @package qbehaviour
* @subpackage adaptive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['disregardedwithoutpenalty'] = 'The submission was invalid, and has been disregarded without penalty.';
$string['gradingdetails'] = 'Marks for this submission: {$a->raw}/{$a->max}.';
$string['gradingdetailswithadjustment'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives <strong>{$a->cur}/{$a->max}</strong>.';
$string['gradingdetailswithadjustmentpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives <strong>{$a->cur}/{$a->max}</strong>. This submission attracted a penalty of {$a->penalty}.';
$string['gradingdetailswithadjustmenttotalpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives <strong>{$a->cur}/{$a->max}</strong>. This submission attracted a penalty of {$a->penalty}. Total penalties so far: {$a->totalpenalty}.';
$string['gradingdetailswithpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. This submission attracted a penalty of {$a->penalty}.';
$string['gradingdetailswithtotalpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. This submission attracted a penalty of {$a->penalty}. Total penalties so far: {$a->totalpenalty}.';
$string['notcomplete'] = 'Not complete';
$string['pluginname'] = 'Adaptive mode';
$string['privacy:metadata'] = 'The Adaptive mode question behaviour plugin does not store any personal data.';
// Old strings these are currently only used in the unit tests, to verify that the new
// strings give the same results as the old strings.
$string['gradingdetailsadjustment'] = 'Accounting for previous tries, this gives <strong>{$a->cur}/{$a->max}</strong>.';
$string['gradingdetailspenalty'] = 'This submission attracted a penalty of {$a}.';
$string['gradingdetailspenaltytotal'] = 'Total penalties so far: {$a}.';
+121
View File
@@ -0,0 +1,121 @@
<?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 legacy
* adaptive behaviour.
*
* @package qbehaviour
* @subpackage adaptive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the legacy
* adaptive behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptive_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
return $this->submit_button($qa, $options);
}
public function feedback(question_attempt $qa, question_display_options $options) {
// If the latest answer was invalid, display an informative message.
if ($qa->get_state() == question_state::$invalid) {
return html_writer::nonempty_tag('div', $this->disregarded_info(),
array('class' => 'gradingdetails'));
}
// Otherwise get the details.
return $this->render_adaptive_marks(
$qa->get_behaviour()->get_adaptive_marks(), $options);
}
/**
* Display the scoring information about an adaptive attempt.
* @param qbehaviour_adaptive_mark_details contains all the score details we need.
* @param question_display_options $options display options.
*/
public function render_adaptive_marks(qbehaviour_adaptive_mark_details $details, question_display_options $options) {
if ($details->state == question_state::$todo || $options->marks < question_display_options::MARK_AND_MAX) {
// No grades yet.
return '';
}
// Display the grading details from the last graded state.
$class = $details->state->get_feedback_class();
return html_writer::tag('div', get_string($class, 'question'),
array('class' => 'correctness badge ' . $class))
. html_writer::tag('div', $this->grading_details($details, $options),
array('class' => 'gradingdetails'));
}
/**
* Display the information about the penalty calculations.
* @param qbehaviour_adaptive_mark_details contains all the score details we need.
* @param question_display_options $options display options.
* @return string html fragment
*/
protected function grading_details(qbehaviour_adaptive_mark_details $details, question_display_options $options) {
$mark = $details->get_formatted_marks($options->markdp);
if ($details->currentpenalty == 0 && $details->totalpenalty == 0) {
return get_string('gradingdetails', 'qbehaviour_adaptive', $mark);
}
$output = '';
// Print details of grade adjustment due to penalties
if ($details->rawmark != $details->actualmark) {
if (!$details->improvable) {
return get_string('gradingdetailswithadjustment', 'qbehaviour_adaptive', $mark);
} else if ($details->totalpenalty > $details->currentpenalty) {
return get_string('gradingdetailswithadjustmenttotalpenalty', 'qbehaviour_adaptive', $mark);
} else {
return get_string('gradingdetailswithadjustmentpenalty', 'qbehaviour_adaptive', $mark);
}
} else {
if (!$details->improvable) {
return get_string('gradingdetails', 'qbehaviour_adaptive', $mark);
} else if ($details->totalpenalty > $details->currentpenalty) {
return get_string('gradingdetailswithtotalpenalty', 'qbehaviour_adaptive', $mark);
} else {
return get_string('gradingdetailswithpenalty', 'qbehaviour_adaptive', $mark);
}
}
return $output;
}
/**
* Display information about a disregarded (incomplete) response.
*/
protected function disregarded_info() {
return get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive');
}
}
@@ -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_adaptive;
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 adaptive behaviour type class.
*
* @package qbehaviour_adaptive
* @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_adaptive_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('adaptive');
}
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->assertFalse($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,106 @@
<?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_adaptive;
use qbehaviour_adaptive_mark_details;
use question_display_options;
use question_state;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
require_once(__DIR__ . '/../behaviour.php');
/**
* Unit tests for the adaptive behaviour the display of mark/penalty information.
*
* @package qbehaviour_adaptive
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mark_display_test extends \basic_testcase {
/** @var qbehaviour_adaptive_renderer the renderer to test. */
protected $renderer;
/** @var question_display_options display options to use when rendering. */
protected $options;
protected function setUp(): void {
global $PAGE;
parent::setUp();
$this->renderer = $PAGE->get_renderer('qbehaviour_adaptive');
$this->options = new question_display_options();
}
public function test_blank_before_graded(): void {
$this->assertEquals('',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$todo), $this->options));
}
public function test_correct_no_penalty(): void {
$this->assertEquals('<div class="correctness badge correct">' . get_string('correct', 'question') . '</div>' .
'<div class="gradingdetails">' .
get_string('gradingdetails', 'qbehaviour_adaptive',
array('cur' => '1.00', 'raw' => '1.00', 'max' => '1.00')) . '</div>',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$gradedright, 1, 1, 1, 0, 0, false), $this->options));
}
public function test_partial_first_try(): void {
$this->assertEquals('<div class="correctness badge partiallycorrect">' . get_string('partiallycorrect', 'question') .
'</div><div class="gradingdetails">' .
get_string('gradingdetails', 'qbehaviour_adaptive',
array('cur' => '0.50', 'raw' => '0.50', 'max' => '1.00')) . ' ' .
get_string('gradingdetailspenalty', 'qbehaviour_adaptive', '0.10') . '</div>',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$gradedpartial, 1, 0.5, 0.5, 0.1, 0.1, true), $this->options));
}
public function test_partial_second_try(): void {
$mark = array('cur' => '0.80', 'raw' => '0.90', 'max' => '1.00');
$this->assertEquals('<div class="correctness badge partiallycorrect">' . get_string('partiallycorrect', 'question') .
'</div><div class="gradingdetails">' .
get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' .
get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . ' ' .
get_string('gradingdetailspenalty', 'qbehaviour_adaptive', '0.10') . ' ' .
get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', '0.20') . '</div>',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$gradedpartial, 1, 0.8, 0.9, 0.1, 0.2, true), $this->options));
}
public function test_correct_third_try(): void {
$mark = array('cur' => '0.80', 'raw' => '1.00', 'max' => '1.00');
$this->assertEquals('<div class="correctness badge partiallycorrect">' . get_string('partiallycorrect', 'question') .
'</div><div class="gradingdetails">' .
get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' .
get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . '</div>',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$gradedpartial, 1, 0.8, 1.0, 0.1, 0.3, false), $this->options));
}
public function test_correct_third_try_if_we_dont_increase_penalties_for_wrong(): void {
$mark = array('cur' => '0.80', 'raw' => '1.00', 'max' => '1.00');
$this->assertEquals('<div class="correctness badge partiallycorrect">' . get_string('partiallycorrect', 'question') .
'</div><div class="gradingdetails">' .
get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' .
get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . '</div>',
$this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details(
question_state::$gradedpartial, 1, 0.8, 1.0, 0, 0.2, false), $this->options));
}
}
@@ -0,0 +1,852 @@
<?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_adaptive;
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 adaptive behaviour.
*
* @package qbehaviour_adaptive
* @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 {
protected function get_contains_penalty_info_expectation($penalty) {
$penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
format_float($penalty, $this->displayoptions->markdp));
return new \question_pattern_expectation('/'.preg_quote($penaltyinfo, '/').'/');
}
protected function get_does_not_contain_penalty_info_expectation() {
$penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive', 'XXXXX');
$penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo, '/')).'/';
return new \question_no_pattern_expectation($penaltypattern);
}
protected function get_contains_total_penalty_expectation($penalty) {
$penaltyinfo = get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive',
format_float($penalty, $this->displayoptions->markdp));
return new \question_pattern_expectation('/'.preg_quote($penaltyinfo, '/').'/');
}
protected function get_does_not_contain_total_penalty_expectation() {
$penaltyinfo = get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', 'XXXXX');
$penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo, '/')).'/';
return new \question_no_pattern_expectation($penaltypattern);
}
protected function get_contains_disregarded_info_expectation() {
$penaltyinfo = get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive');
return new \question_pattern_expectation('/'.preg_quote($penaltyinfo, '/').'/');
}
protected function get_does_not_contain_disregarded_info_expectation() {
$penaltyinfo = get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive');
return new \question_no_pattern_expectation('/'.preg_quote($penaltyinfo, '/').'/');
}
public function test_adaptive_multichoice(): void {
// Create a multiple choice, single response question.
$mc = \test_question_maker::make_a_multichoice_single_question();
$mc->penalty = 0.3333333;
$this->start_attempt_at_question($mc, 'adaptive', 3);
$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());
// Process a submit.
$this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$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 + 2) % 3, true, false),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(1.00),
$this->get_does_not_contain_total_penalty_expectation());
$this->assertMatchesRegularExpression('/B|C/',
$this->quba->get_response_summary($this->slot));
// Process a change of answer to the right one, but not sumbitted.
$this->process_submission(array('answer' => $rightindex));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_mc_radio_expectation($rightindex, true, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
$this->assertMatchesRegularExpression('/B|C/',
$this->quba->get_response_summary($this->slot));
// Now submit the right answer.
$this->process_submission(array('answer' => $rightindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(3 * (1 - $mc->penalty));
$this->check_current_output(
$this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
$this->get_contains_mc_radio_expectation($rightindex, true, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation());
$this->assertEquals('A',
$this->quba->get_response_summary($this->slot));
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3 * (1 - $mc->penalty));
$this->check_current_output(
$this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
$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 + 2) % 3, false, false),
$this->get_contains_correct_expectation());
// Process a manual comment.
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
$mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
$mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
$this->get_contains_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), 1, 0.0000001);
}
public function test_adaptive_multichoice2(): void {
// Create a multiple choice, multiple response question.
$mc = \test_question_maker::make_a_multichoice_multi_question();
$mc->penalty = 0.3333333;
$mc->shuffleanswers = 0;
$this->start_attempt_at_question($mc, 'adaptive', 2);
// 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_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation());
// Process a submit.
$this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_mark_summary(2),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation());
// Save the same correct answer again. Should not do anything.
$numsteps = $this->get_step_count();
$this->process_submission(array('choice0' => 1, 'choice2' => 1));
// Verify.
$this->check_step_count($numsteps);
$this->check_current_mark(2);
$this->check_current_state(question_state::$complete);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_mark_summary(2),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation());
}
public function test_adaptive_shortanswer_partially_right(): void {
// Create a short answer question.
$sa = \test_question_maker::make_question('shortanswer');
$this->start_attempt_at_question($sa, 'adaptive');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->render();
$this->check_output_does_not_contain_text_input_with_class('answer', 'correct');
$this->check_output_does_not_contain_text_input_with_class('answer', 'partiallycorrect');
$this->check_output_does_not_contain_text_input_with_class('answer', 'incorrect');
$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());
// Submit a partially correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0.8);
$this->render();
$this->check_output_contains_text_input_with_class('answer', 'partiallycorrect');
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_partcorrect_expectation(),
$this->get_contains_penalty_info_expectation(0.33),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit an incorrect answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0.8);
$this->render();
$this->check_output_contains_text_input_with_class('answer', 'incorrect');
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(0.33),
$this->get_contains_total_penalty_expectation(0.67),
$this->get_does_not_contain_validation_error_expectation());
// Submit a correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(0.8);
$this->render();
$this->check_output_contains_text_input_with_class('answer', 'correct');
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(0.8);
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
public function test_adaptive_shortanswer_wrong_right_wrong(): void {
// Create a short answer question.
$sa = \test_question_maker::make_question('shortanswer');
$this->start_attempt_at_question($sa, 'adaptive', 6);
// 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());
// Submit a wrong answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(2.00),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit the same wrong answer again. Nothing should change.
$this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(2.00),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit a correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(4.00);
$this->check_current_output(
$this->get_contains_mark_summary(4.00),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit another incorrect answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(4.00);
$this->check_current_output(
$this->get_contains_mark_summary(4.00),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(4.00);
$this->check_current_output(
$this->get_contains_mark_summary(4.00),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
public function test_adaptive_shortanswer_invalid_after_complete(): void {
// Create a short answer question.
$sa = \test_question_maker::make_question('shortanswer');
$this->start_attempt_at_question($sa, 'adaptive');
// 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());
// Submit a wrong answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(0.33),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit a correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(0.66666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.67),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit an empty answer.
$this->process_submission(array('-submit' => 1, 'answer' => ''));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(0.66666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.67),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation());
// Submit another wrong answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(0.66666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.67),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(0.66666667);
$this->check_current_output(
$this->get_contains_mark_summary(0.67),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
public function test_adaptive_shortanswer_zero_penalty(): void {
// Create a short answer question.
$sa = \test_question_maker::make_question('shortanswer');
// Disable penalties for this question.
$sa->penalty = 0;
$this->start_attempt_at_question($sa, 'adaptive');
// 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());
// Submit a wrong answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit a correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(1.0);
$this->check_current_output(
$this->get_contains_mark_summary(1.0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(1.0);
$this->check_current_output(
$this->get_contains_mark_summary(1.0),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
public function test_adaptive_shortanswer_try_to_submit_blank(): void {
// Create a short answer question with correct answer true.
$sa = \test_question_maker::make_question('shortanswer');
$this->start_attempt_at_question($sa, 'adaptive');
// 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());
// Submit with blank answer.
$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_correctness_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_contains_disregarded_info_expectation());
$this->assertNull($this->quba->get_response_summary($this->slot));
// Now get it wrong.
$this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0.8);
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_partcorrect_expectation(),
$this->get_contains_penalty_info_expectation(0.33),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Now submit blank again.
$this->process_submission(array('-submit' => 1, 'answer' => ''));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(0.8);
$this->check_current_output(
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation());
}
public function test_adaptive_numerical(): void {
// Create a numerical question.
$sa = \test_question_maker::make_question('numerical', 'pi');
$this->start_attempt_at_question($sa, 'adaptive');
// 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());
// Submit the correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit an incorrect answer.
$this->process_submission(array('-submit' => 1, 'answer' => '-5'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
public function test_adaptive_numerical_invalid(): void {
// Create a numerical question.
$numq = \test_question_maker::make_question('numerical', 'pi');
$numq->penalty = 0.1;
$this->start_attempt_at_question($numq, 'adaptive');
// 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());
// Submit a non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(1),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_contains_disregarded_info_expectation());
// Submit an incorrect answer.
$this->process_submission(array('-submit' => 1, 'answer' => '-5'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(0.1),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_disregarded_info_expectation());
// Submit another non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi*2'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_contains_disregarded_info_expectation());
// Submit the correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(0.9);
$this->check_current_output(
$this->get_contains_mark_summary(0.9),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_disregarded_info_expectation());
// Submit another non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi/3'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(0.9);
$this->check_current_output(
$this->get_contains_mark_summary(0.9),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_contains_disregarded_info_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(0.9);
$this->check_current_output(
$this->get_contains_mark_summary(0.9),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_disregarded_info_expectation());
}
public function test_adaptive_multianswer(): void {
// Create a multianswer question.
$q = \test_question_maker::make_question('multianswer', 'twosubq');
// To simplify testing, multichoice subquestion's answers are not shuffled.
$q->subquestions[2]->shuffleanswers = 0;
$choices = array('0' => 'Bow-wow', '1' => 'Wiggly worm', '2' => 'Pussy-cat');
$this->start_attempt_at_question($q, 'adaptive', 12);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->assertEquals('adaptive',
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
$this->render();
$this->check_output_contains_text_input('sub1_answer', '', true);
$this->check_output_does_not_contain_text_input_with_class('sub1_answer', 'correct');
$this->check_output_does_not_contain_text_input_with_class('sub1_answer', 'partiallycorrect');
$this->check_output_does_not_contain_text_input_with_class('sub1_answer', 'incorrect');
$this->check_current_output(
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_feedback_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub2_answer', $choices, null, true));
// Submit an invalid response.
$this->process_submission(array('sub1_answer' => '', 'sub2_answer' => 1, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_output_contains_text_input('sub1_answer', '', true);
$this->check_current_output(
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_contains_disregarded_info_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub2_answer', $choices, 1, true));
// Check that extract responses will return the reset data.
$prefix = $this->quba->get_field_prefix($this->slot);
$this->assertEquals(array('sub2_answer' => 1),
$this->quba->extract_responses($this->slot, array($prefix . 'sub2_answer' => 1)));
// Submit an incorrect response.
$this->process_submission(array('sub1_answer' => 'Dog',
'sub2_answer' => 1, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->render();
$this->check_output_contains_text_input('sub1_answer', 'Dog', true);
$this->check_output_contains_text_input_with_class('sub1_answer', 'incorrect');
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_contains_penalty_info_expectation(4.00),
$this->get_does_not_contain_validation_error_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub2_answer', $choices, 1, true));
// Submit the right answer.
$this->process_submission(array('sub1_answer' => 'Owl', 'sub2_answer' => 2, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(8.00);
$this->render();
$this->check_output_contains_text_input('sub1_answer', 'Owl', true);
$this->check_output_contains_text_input_with_class('sub1_answer', 'correct');
$this->check_current_output(
$this->get_contains_mark_summary(8.00),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub2_answer', $choices, '2', true));
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(8.00);
$this->render();
$this->check_output_contains_text_input('sub1_answer', 'Owl', false);
$this->check_output_contains_text_input_with_class('sub1_answer', 'correct');
$this->check_current_output(
$this->get_contains_mark_summary(8.00),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
}
+33
View File
@@ -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 adaptive
* @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_adaptive';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,44 @@
<?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 for the old adaptive mode, with no penalties.
*
* @package qbehaviour
* @subpackage adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../adaptive/behaviour.php');
/**
* Question behaviour for adaptive mode, with no penalties.
*
* This is the old version of interactive mode, without penalties.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptivenopenalty extends qbehaviour_adaptive {
protected function adjusted_fraction($fraction, $prevtries) {
return $fraction;
}
}
@@ -0,0 +1,38 @@
<?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 adaptive behaviour, without penalties.
*
* @package qbehaviour_adaptivenopenalty
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../adaptive/behaviourtype.php');
/**
* Question behaviour type information for adaptive behaviour, without penalties.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptivenopenalty_type extends qbehaviour_adaptive_type {
}
@@ -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_adaptivenopenalty.
*
* @package qbehaviour_adaptivenopenalty
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_adaptivenopenalty\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_adaptivenopenalty 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,27 @@
<?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_adaptivenopenalty', language 'en'.
*
* @package qbehaviour
* @subpackage adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Adaptive mode (no penalties)';
$string['privacy:metadata'] = 'The Adaptive mode (no penalties) question behaviour plugin does not store any personal data.';
@@ -0,0 +1,49 @@
<?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 legacy
* adaptive (no penalties) behaviour.
*
* @package qbehaviour
* @subpackage adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../adaptive/renderer.php');
/**
* Renderer for outputting parts of a question belonging to the legacy
* adaptive (no penalties) behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptivenopenalty_renderer extends qbehaviour_adaptive_renderer {
protected function grading_details(qbehaviour_adaptive_mark_details $details, question_display_options $options) {
$mark = $details->get_formatted_marks($options->markdp);
return get_string('gradingdetails', 'qbehaviour_adaptive', $mark);
}
protected function disregarded_info() {
return '';
}
}
@@ -0,0 +1,306 @@
<?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_adaptivenopenalty;
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 adaptive (no penalties) behaviour.
*
* @package qbehaviour_adaptivenopenalty
* @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 {
protected function get_does_not_contain_gradingdetails_expectation() {
return new \question_no_pattern_expectation('/class="gradingdetails"/');
}
protected function get_does_not_contain_penalty_info_expectation() {
$penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive', 'XXXXX');
$penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo, '/')).'/';
return new \question_no_pattern_expectation($penaltypattern);
}
protected function get_does_not_contain_total_penalty_expectation() {
$penaltyinfo = get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', 'XXXXX');
$penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo, '/')).'/';
return new \question_no_pattern_expectation($penaltypattern);
}
public function test_multichoice(): void {
// Create a multiple choice, single response question.
$mc = \test_question_maker::make_a_multichoice_single_question();
$mc->penalty = 0.3333333;
$this->start_attempt_at_question($mc, 'adaptivenopenalty', 3);
$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());
// Process a submit.
$this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$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 + 2) % 3, true, false),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation());
$this->assertMatchesRegularExpression('/B|C/',
$this->quba->get_response_summary($this->slot));
// Process a change of answer to the right one, but not sumbitted.
$this->process_submission(array('answer' => $rightindex));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_mc_radio_expectation($rightindex, true, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
$this->assertMatchesRegularExpression('/B|C/',
$this->quba->get_response_summary($this->slot));
// Now submit the right answer.
$this->process_submission(array('answer' => $rightindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mark_summary(3),
$this->get_contains_mc_radio_expectation($rightindex, true, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation());
$this->assertEquals('A',
$this->quba->get_response_summary($this->slot));
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mark_summary(3),
$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 + 2) % 3, false, false),
$this->get_contains_correct_expectation());
// Process a manual comment.
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
$mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
$mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
$this->get_contains_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 3);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), 1, 0.0000001);
}
public function test_multichoice2(): void {
// Create a multiple choice, multiple response question.
$mc = \test_question_maker::make_a_multichoice_multi_question();
$mc->penalty = 0.3333333;
$mc->shuffleanswers = 0;
$this->start_attempt_at_question($mc, 'adaptivenopenalty', 2);
// 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_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation());
// Process a submit.
$this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_mark_summary(2),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation());
// Save the same correct answer again. Should no do anything.
$numsteps = $this->get_step_count();
$this->process_submission(array('choice0' => 1, 'choice2' => 1));
// Verify.
$this->check_step_count($numsteps);
$this->check_current_state(question_state::$complete);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output(
$this->get_contains_mark_summary(2),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation());
}
public function test_numerical_invalid(): void {
// Create a numerical question
$numq = \test_question_maker::make_question('numerical', 'pi');
$numq->penalty = 0.1;
$this->start_attempt_at_question($numq, 'adaptivenopenalty');
// 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());
// Submit a non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_marked_out_of_summary(1),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Submit an incorrect answer.
$this->process_submission(array('-submit' => 1, 'answer' => '-5'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_penalty_info_expectation(),
$this->get_does_not_contain_total_penalty_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit another non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi*2'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(0);
$this->check_current_output(
$this->get_contains_mark_summary(0),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_gradingdetails_expectation());
// Submit the correct answer.
$this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(1.0);
$this->check_current_output(
$this->get_contains_mark_summary(1.0),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation());
// Submit another non-numerical answer.
$this->process_submission(array('-submit' => 1, 'answer' => 'Pi/3'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(1.0);
$this->check_current_output(
$this->get_contains_mark_summary(1.0),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_gradingdetails_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(1.0);
$this->check_current_output(
$this->get_contains_mark_summary(1.0),
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_incorrect_expectation(),
$this->get_does_not_contain_validation_error_expectation());
}
}
@@ -0,0 +1,36 @@
<?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 adaptivenopenalty
* @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_adaptivenopenalty';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->dependencies = [
'qbehaviour_adaptive' => 2024041600,
];
$plugin->maturity = MATURITY_STABLE;
+838
View File
@@ -0,0 +1,838 @@
<?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/>.
/**
* Defines the question behaviour base class
*
* @package moodlecore
* @subpackage questionbehaviours
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The base class for question behaviours.
*
* A question behaviour is used by the question engine, specifically by
* a {@link question_attempt} to manage the flow of actions a student can take
* as they work through a question, and later, as a teacher manually grades it.
* In turn, the behaviour will delegate certain processing to the
* relevant {@link question_definition}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_behaviour {
/** @var question_attempt the question attempt we are managing. */
protected $qa;
/** @var question_definition shortcut to $qa->get_question(). */
protected $question;
/**
* Normally you should not call this constuctor directly. The appropriate
* behaviour object is created automatically as part of
* {@link question_attempt::start()}.
* @param question_attempt $qa the question attempt we will be managing.
* @param string $preferredbehaviour the type of behaviour that was actually
* requested. This information is not needed in most cases, the type of
* subclass is enough, but occasionally it is needed.
*/
public function __construct(question_attempt $qa, $preferredbehaviour) {
$this->qa = $qa;
$this->question = $qa->get_question(false);
if (!$this->is_compatible_question($this->question)) {
throw new coding_exception('This behaviour (' . $this->get_name() .
') cannot work with this question (' . get_class($this->question) . ')');
}
}
/**
* Some behaviours can only work with certing types of question. This method
* allows the behaviour to verify that a question is compatible.
*
* This implementation is only provided for backwards-compatibility. You should
* override this method if you are implementing a behaviour.
*
* @param question_definition $question the question.
*/
abstract public function is_compatible_question(question_definition $question);
/**
* @return string the name of this behaviour. For example the name of
* qbehaviour_mymodle is 'mymodel'.
*/
public function get_name() {
return substr(get_class($this), 11);
}
/**
* Whether the current attempt at this question could be completed just by the
* student interacting with the question, before $qa->finish() is called.
*
* @return boolean whether the attempt can finish naturally.
*/
public function can_finish_during_attempt() {
return false;
}
/**
* Cause the question to be renderered. This gets the appropriate behaviour
* renderer using {@link get_renderer()}, and adjusts the display
* options using {@link adjust_display_options()} and then calls
* {@link core_question_renderer::question()} to do the work.
* @param question_display_options $options controls what should and should not be displayed.
* @param string|null $number the question number to display.
* @param core_question_renderer $qoutput the question renderer that will coordinate everything.
* @param qtype_renderer $qtoutput the question type renderer that will be helping.
* @return string HTML fragment.
*/
public function render(question_display_options $options, $number,
core_question_renderer $qoutput, qtype_renderer $qtoutput) {
$behaviouroutput = $this->get_renderer($qoutput->get_page());
$options = clone($options);
$this->adjust_display_options($options);
return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
}
/**
* Checks whether the users is allow to be served a particular file.
* @param question_display_options $options the options that control display of the question.
* @param string $component the name of the component we are serving files for.
* @param string $filearea the name of the file area.
* @param array $args the remaining bits of the file path.
* @param bool $forcedownload whether the user must be forced to download the file.
* @return bool true if the user can access this file.
*/
public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
$this->adjust_display_options($options);
if ($component == 'question' && $filearea == 'response_bf_comment') {
foreach ($this->qa->get_step_iterator() as $attemptstep) {
if ($attemptstep->get_id() == $args[0]) {
return true;
}
}
return false;
}
return $this->question->check_file_access($this->qa, $options, $component,
$filearea, $args, $forcedownload);
}
/**
* @param moodle_page $page the page to render for.
* @return qbehaviour_renderer get the appropriate renderer to use for this model.
*/
public function get_renderer(moodle_page $page) {
return $page->get_renderer(get_class($this));
}
/**
* Make any changes to the display options before a question is rendered, so
* that it can be displayed in a way that is appropriate for the statue it is
* currently in. For example, by default, if the question is finished, we
* ensure that it is only ever displayed read-only.
* @param question_display_options $options the options to adjust. Just change
* the properties of this object - objects are passed by referece.
*/
public function adjust_display_options(question_display_options $options) {
if (!$this->qa->has_marks()) {
$options->correctness = false;
$options->numpartscorrect = false;
}
if ($this->qa->get_state()->is_finished()) {
$options->readonly = true;
$options->numpartscorrect = $options->numpartscorrect &&
$this->qa->get_state()->is_partially_correct() &&
!empty($this->question->shownumcorrect);
} else {
$options->hide_all_feedback();
}
}
/**
* Get the most applicable hint for the question in its current state.
* @return question_hint the most applicable hint, or null, if none.
*/
public function get_applicable_hint() {
return null;
}
/**
* What is the minimum fraction that can be scored for this question.
* Normally this will be based on $this->question->get_min_fraction(),
* but may be modified in some way by the behaviour.
*
* @return number the minimum fraction when this question is attempted under
* this behaviour.
*/
public function get_min_fraction() {
return 0;
}
/**
* Return the maximum possible fraction that can be scored for this question.
* Normally this will be based on $this->question->get_max_fraction(),
* but may be modified in some way by the behaviour.
*
* @return number the maximum fraction when this question is attempted under
* this behaviour.
*/
public function get_max_fraction() {
return $this->question->get_max_fraction();
}
/**
* Return an array of the behaviour variables that could be submitted
* as part of a question of this type, with their types, so they can be
* properly cleaned.
* @return array variable name => PARAM_... constant.
*/
public function get_expected_data() {
if (!$this->qa->get_state()->is_finished()) {
return array();
}
$vars = array('comment' => question_attempt::PARAM_RAW_FILES, 'commentformat' => PARAM_INT);
if ($this->qa->get_max_mark()) {
$vars['mark'] = PARAM_RAW_TRIMMED;
$vars['maxmark'] = PARAM_FLOAT;
}
return $vars;
}
/**
* Return an array of question type variables for the question in its current
* state. Normally, if {@link adjust_display_options()} would set
* {@link question_display_options::$readonly} to true, then this method
* should return an empty array, otherwise it should return
* $this->question->get_expected_data(). Thus, there should be little need to
* override this method.
* @return array|string variable name => PARAM_... constant, or, as a special case
* that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
* meaning take all the raw submitted data belonging to this question.
*/
public function get_expected_qt_data() {
$fakeoptions = new question_display_options();
$fakeoptions->readonly = false;
$this->adjust_display_options($fakeoptions);
if ($fakeoptions->readonly) {
return array();
} else {
return $this->question->get_expected_data();
}
}
/**
* Return an array of any im variables, and the value required to get full
* marks.
* @return array variable name => value.
*/
public function get_correct_response() {
return array();
}
/**
* Generate a brief, plain-text, summary of this question. This is used by
* various reports. This should show the particular variant of the question
* as presented to students. For example, the calculated quetsion type would
* fill in the particular numbers that were presented to the student.
* This method will return null if such a summary is not possible, or
* inappropriate.
*
* Normally, this method delegates to {question_definition::get_question_summary()}.
*
* @return string|null a plain text summary of this question.
*/
public function get_question_summary() {
return $this->question->get_question_summary();
}
/**
* Generate a brief, plain-text, summary of the correct answer to this question.
* This is used by various reports, and can also be useful when testing.
* This method will return null if such a summary is not possible, or
* inappropriate.
*
* @return string|null a plain text summary of the right answer to this question.
*/
public function get_right_answer_summary() {
return null;
}
/**
* Used by {@link start_based_on()} to get the data needed to start a new
* attempt from the point this attempt has go to.
* @return array name => value pairs.
*/
public function get_resume_data() {
$olddata = $this->qa->get_step(0)->get_all_data();
$olddata = $this->qa->get_last_qt_data() + $olddata;
$olddata = $this->get_our_resume_data() + $olddata;
return $olddata;
}
/**
* Used by {@link start_based_on()} to get the data needed to start a new
* attempt from the point this attempt has go to.
* @return unknown_type
*/
protected function get_our_resume_data() {
return array();
}
/**
* Classify responses for this question into a number of sub parts and response classes as defined by
* {@link \question_type::get_possible_responses} for this question type.
*
* @param string $whichtries which tries to analyse for response analysis. Will be one of
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* Defaults to question_attempt::LAST_TRY.
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
* and values are question_classified_response instances.
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
* and the second key is subpartid.
*/
public function classify_response($whichtries = question_attempt::LAST_TRY) {
if ($whichtries == question_attempt::LAST_TRY) {
return $this->question->classify_response($this->qa->get_last_qt_data());
} else {
$stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
if ($whichtries == question_attempt::FIRST_TRY) {
$firsttry = $stepswithsubmit[1];
if ($firsttry) {
return $this->question->classify_response($firsttry->get_qt_data());
} else {
return $this->question->classify_response(array());
}
} else {
$classifiedresponses = array();
foreach ($stepswithsubmit as $submittedresponseno => $step) {
$classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
}
return $classifiedresponses;
}
}
}
/**
* Generate a brief textual description of the current state of the question,
* normally displayed under the question number.
*
* @param bool $showcorrectness Whether right/partial/wrong states should
* be distinguised.
* @return string a brief summary of the current state of the qestion attempt.
*/
public function get_state_string($showcorrectness) {
return $this->qa->get_state()->default_string($showcorrectness);
}
abstract public function summarise_action(question_attempt_step $step);
/**
* Initialise the first step in a question attempt when a new
* {@link question_attempt} is being started.
*
* This method must call $this->question->start_attempt($step, $variant), and may
* perform additional processing if the behaviour requries it.
*
* @param question_attempt_step $step the first step of the
* question_attempt being started.
* @param int $variant which variant of the question to use.
*/
public function init_first_step(question_attempt_step $step, $variant) {
$this->question->start_attempt($step, $variant);
$step->set_state(question_state::$todo);
}
/**
* When an attempt is started based on a previous attempt (see
* {@link question_attempt::start_based_on}) this method is called to setup
* the new attempt.
*
* This method must call $this->question->apply_attempt_state($step), and may
* perform additional processing if the behaviour requries it.
*
* @param question_attempt_step The first step of the {@link question_attempt}
* being loaded.
*/
public function apply_attempt_state(question_attempt_step $step) {
$this->question->apply_attempt_state($step);
$step->set_state(question_state::$todo);
}
/**
* Checks whether two manual grading actions are the same. That is, whether
* the comment, and the mark (if given) is the same.
*
* @param question_attempt_step $pendingstep contains the new responses.
* @return bool whether the new response is the same as we already have.
*/
protected function is_same_comment($pendingstep) {
$previouscomment = $this->qa->get_last_behaviour_var('comment');
$newcomment = $pendingstep->get_behaviour_var('comment');
// When the teacher leaves the comment empty, $previouscomment is an empty string but $newcomment is null,
// therefore they are not equal to each other. That's why checking if $previouscomment != $newcomment is not enough.
if (($previouscomment != $newcomment) && !(is_null($previouscomment) && html_is_blank($newcomment))) {
// The comment has changed.
return false;
}
if (!html_is_blank($newcomment)) {
// Check comment format.
$previouscommentformat = $this->qa->get_last_behaviour_var('commentformat');
$newcommentformat = $pendingstep->get_behaviour_var('commentformat');
if ($previouscommentformat != $newcommentformat) {
return false;
}
}
// So, now we know the comment is the same, so check the mark, if present.
$previousfraction = $this->qa->get_fraction();
$newmark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
if (is_null($previousfraction)) {
return is_null($newmark) || $newmark === '';
} else if (is_null($newmark) || $newmark === '') {
return false;
}
$newfraction = $newmark / $pendingstep->get_behaviour_var('maxmark');
return abs($newfraction - $previousfraction) < 0.0000001;
}
/**
* The main entry point for processing an action.
*
* All the various operations that can be performed on a
* {@link question_attempt} get channeled through this function, except for
* {@link question_attempt::start()} which goes to {@link init_first_step()}.
* {@link question_attempt::finish()} becomes an action with im vars
* finish => 1, and manual comment/grade becomes an action with im vars
* comment => comment text, and mark => ..., max_mark => ... if the question
* is graded.
*
* This method should first determine whether the action is significant. For
* example, if no actual action is being performed, but instead the current
* responses are being saved, and there has been no change since the last
* set of responses that were saved, this the action is not significatn. In
* this case, this method should return {@link question_attempt::DISCARD}.
* Otherwise it should return {@link question_attempt::KEEP}.
*
* If the action is significant, this method should also perform any
* necessary updates to $pendingstep. For example, it should call
* {@link question_attempt_step::set_state()} to set the state that results
* from this action, and if this is a grading action, it should call
* {@link question_attempt_step::set_fraction()}.
*
* This method can also call {@link question_attempt_step::set_behaviour_var()} to
* store additional infomation. There are two main uses for this. This can
* be used to store the result of any randomisation done. It is important to
* store the result of randomisation once, and then in future use the same
* outcome if the actions are ever replayed. This is how regrading works.
* The other use is to cache the result of expensive computations performed
* on the raw response data, so that subsequent display and review of the
* question does not have to repeat the same expensive computations.
*
* Often this method is implemented as a dispatching method that examines
* the pending step to determine the kind of action being performed, and
* then calls a more specific method like {@link process_save()} or
* {@link process_comment()}. Look at some of the standard behaviours
* for examples.
*
* @param question_attempt_pending_step $pendingstep a partially initialised step
* containing all the information about the action that is being peformed. This
* information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
*/
abstract public function process_action(question_attempt_pending_step $pendingstep);
/**
* Auto-saved data. By default this does nothing. interesting processing is
* done in {@link question_behaviour_with_save}.
*
* @param question_attempt_pending_step $pendingstep a partially initialised step
* containing all the information about the action that is being peformed. This
* information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
*/
public function process_autosave(question_attempt_pending_step $pendingstep) {
return question_attempt::DISCARD;
}
/**
* Implementation of processing a manual comment/grade action that should
* be suitable for most subclasses.
* @param question_attempt_pending_step $pendingstep a partially initialised step
* containing all the information about the action that is being peformed.
* @return bool either {@link question_attempt::KEEP}
*/
public function process_comment(question_attempt_pending_step $pendingstep) {
if (!$this->qa->get_state()->is_finished()) {
throw new coding_exception('Cannot manually grade a question before it is finshed.');
}
if ($this->is_same_comment($pendingstep)) {
return question_attempt::DISCARD;
}
if ($pendingstep->has_behaviour_var('mark')) {
$mark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
if ($mark === null) {
throw new coding_exception('Inalid number format ' . $pendingstep->get_behaviour_var('mark') .
' when processing a manual grading action.', 'Question ' . $this->question->id .
', slot ' . $this->qa->get_slot());
} else if ($mark === '') {
$fraction = null;
} else {
$fraction = $mark / $pendingstep->get_behaviour_var('maxmark');
if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
throw new coding_exception('Score out of range when processing ' .
'a manual grading action.', 'Question ' . $this->question->id .
', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
}
}
$pendingstep->set_fraction($fraction);
}
$pendingstep->set_state($this->qa->get_state()->corresponding_commented_state(
$pendingstep->get_fraction()));
return question_attempt::KEEP;
}
/**
* @param $comment the comment text to format. If omitted,
* $this->qa->get_manual_comment() is used.
* @param $commentformat the format of the comment, one of the FORMAT_... constants.
* @param $context the quiz context.
* @return string the comment, ready to be output.
*/
public function format_comment($comment = null, $commentformat = null, $context = null) {
$formatoptions = new stdClass();
$formatoptions->noclean = true;
$formatoptions->para = false;
if (is_null($comment)) {
list($comment, $commentformat, $commentstep) = $this->qa->get_manual_comment();
}
if ($context !== null) {
$comment = $this->qa->rewrite_response_pluginfile_urls($comment, $context->id, 'bf_comment', $commentstep);
}
return format_text($comment, $commentformat, $formatoptions);
}
/**
* @return string a summary of a manual comment action.
* @param question_attempt_step $step
*/
protected function summarise_manual_comment($step) {
$a = new stdClass();
if ($step->has_behaviour_var('comment')) {
$comment = question_utils::to_plain_text($step->get_behaviour_var('comment'),
$step->get_behaviour_var('commentformat'));
$a->comment = shorten_text($comment, 200);
} else {
$a->comment = '';
}
$mark = question_utils::clean_param_mark($step->get_behaviour_var('mark'));
if (is_null($mark) || $mark === '') {
return get_string('commented', 'question', $a->comment);
} else {
$a->mark = $mark / $step->get_behaviour_var('maxmark') * $this->qa->get_max_mark();
return get_string('manuallygraded', 'question', $a);
}
}
public function summarise_start($step) {
return get_string('started', 'question');
}
public function summarise_finish($step) {
return get_string('attemptfinished', 'question');
}
/**
* Does this step include a response submitted by a student?
*
* This method should return true for any attempt explicitly submitted by a student. The question engine itself will also
* automatically recognise any last saved response before the attempt is finished, you don't need to return true here for these
* steps with responses which are not explicitly submitted by the student.
*
* @param question_attempt_step $step
* @return bool is this a step within a question attempt that includes a submitted response by a student.
*/
public function step_has_a_submitted_response($step) {
return false;
}
}
/**
* A subclass of {@link question_behaviour} that implements a save
* action that is suitable for most questions that implement the
* {@link question_manually_gradable} interface.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_behaviour_with_save extends question_behaviour {
public function required_question_definition_type() {
return 'question_manually_gradable';
}
public function apply_attempt_state(question_attempt_step $step) {
parent::apply_attempt_state($step);
if ($this->question->is_complete_response($step->get_qt_data())) {
$step->set_state(question_state::$complete);
}
}
/**
* Work out whether the response in $pendingstep are significantly different
* from the last set of responses we have stored.
* @param question_attempt_step $pendingstep contains the new responses.
* @return bool whether the new response is the same as we already have.
*/
protected function is_same_response(question_attempt_step $pendingstep) {
return $this->question->is_same_response(
$this->qa->get_last_step()->get_qt_data(), $pendingstep->get_qt_data());
}
/**
* Work out whether the response in $pendingstep represent a complete answer
* to the question. Normally this will call
* {@link question_manually_gradable::is_complete_response}, but some
* behaviours, for example the CBM ones, have their own parts to the
* response.
* @param question_attempt_step $pendingstep contains the new responses.
* @return bool whether the new response is complete.
*/
protected function is_complete_response(question_attempt_step $pendingstep) {
return $this->question->is_complete_response($pendingstep->get_qt_data());
}
public function process_autosave(question_attempt_pending_step $pendingstep) {
// If already finished. Nothing to do.
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
// If the new data is the same as we already have, then we don't need it.
if ($this->is_same_response($pendingstep)) {
return question_attempt::DISCARD;
}
// Repeat that test discarding any existing autosaved data.
if ($this->qa->has_autosaved_step()) {
$this->qa->discard_autosaved_step();
if ($this->is_same_response($pendingstep)) {
return question_attempt::DISCARD;
}
}
// OK, we need to save.
return $this->process_save($pendingstep);
}
/**
* Implementation of processing a save action that should be suitable for
* most subclasses.
* @param question_attempt_pending_step $pendingstep a partially initialised step
* containing all the information about the action that is being peformed.
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
*/
public function process_save(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
} else if (!$this->qa->get_state()->is_active()) {
throw new coding_exception('Question is not active, cannot process_actions.');
}
if ($this->is_same_response($pendingstep)) {
return question_attempt::DISCARD;
}
if ($this->is_complete_response($pendingstep)) {
$pendingstep->set_state(question_state::$complete);
} else {
$pendingstep->set_state(question_state::$todo);
}
return question_attempt::KEEP;
}
public function summarise_submit(question_attempt_step $step) {
return get_string('submitted', 'question',
$this->question->summarise_response($step->get_qt_data()));
}
public function summarise_save(question_attempt_step $step) {
$data = $step->get_submitted_data();
if (empty($data)) {
return $this->summarise_start($step);
}
return get_string('saved', 'question',
$this->question->summarise_response($step->get_qt_data()));
}
public function summarise_finish($step) {
$data = $step->get_qt_data();
if ($data) {
return get_string('attemptfinishedsubmitting', 'question',
$this->question->summarise_response($data));
}
return get_string('attemptfinished', 'question');
}
}
abstract class question_behaviour_with_multiple_tries extends question_behaviour_with_save {
public function step_has_a_submitted_response($step) {
return $step->has_behaviour_var('submit') && $step->get_state() != question_state::$invalid;
}
}
/**
* This helper class contains the constants and methods required for
* manipulating scores for certainty based marking.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_cbm {
/**#@+ @var integer named constants for the certainty levels. */
const LOW = 1;
const MED = 2;
const HIGH = 3;
/**#@-*/
/** @var array list of all the certainty levels. */
public static $certainties = array(self::LOW, self::MED, self::HIGH);
/**#@+ @var array coefficients used to adjust the fraction based on certainty. */
protected static $rightscore = array(
self::LOW => 1,
self::MED => 2,
self::HIGH => 3,
);
protected static $wrongscore = array(
self::LOW => 0,
self::MED => -2,
self::HIGH => -6,
);
/**#@-*/
/**#@+ @var array upper and lower limits of the optimal window. */
protected static $lowlimit = array(
self::LOW => 0,
self::MED => 0.666666666666667,
self::HIGH => 0.8,
);
protected static $highlimit = array(
self::LOW => 0.666666666666667,
self::MED => 0.8,
self::HIGH => 1,
);
/**#@-*/
/**
* @return int the default certaintly level that should be assuemd if
* the student does not choose one.
*/
public static function default_certainty() {
return self::LOW;
}
/**
* Given a fraction, and a certainty, compute the adjusted fraction.
* @param number $fraction the raw fraction for this question.
* @param int $certainty one of the certainty level constants.
* @return number the adjusted fraction taking the certainty into account.
*/
public static function adjust_fraction($fraction, $certainty) {
if ($certainty == -1) {
// Certainty -1 has never been used in standard Moodle, but is
// used in Tony-Gardiner Medwin's patches to mean 'No idea' which
// we intend to implement: MDL-42077. In the mean time, avoid
// errors for people who have used TGM's patches.
return 0;
}
if ($fraction <= question_utils::MARK_TOLERANCE) {
return self::$wrongscore[$certainty];
} else {
return self::$rightscore[$certainty] * $fraction;
}
}
/**
* @param int $certainty one of the LOW/MED/HIGH constants.
* @return string a textual description of this certainty.
*/
public static function get_string($certainty) {
return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
}
/**
* @param int $certainty one of the LOW/MED/HIGH constants.
* @return string a short textual description of this certainty.
*/
public static function get_short_string($certainty) {
return get_string('certaintyshort' . $certainty, 'qbehaviour_deferredcbm');
}
/**
* Add information about certainty to a response summary.
* @param string $summary the response summary.
* @param int $certainty the level of certainty to add.
* @return string the summary with information about the certainty added.
*/
public static function summary_with_certainty($summary, $certainty) {
if (is_null($certainty)) {
return $summary;
}
return $summary . ' [' . self::get_short_string($certainty) . ']';
}
/**
* @param int $certainty one of the LOW/MED/HIGH constants.
* @return float the lower limit of the optimal probability range for this certainty.
*/
public static function optimal_probablility_low($certainty) {
return self::$lowlimit[$certainty];
}
/**
* @param int $certainty one of the LOW/MED/HIGH constants.
* @return float the upper limit of the optimal probability range for this certainty.
*/
public static function optimal_probablility_high($certainty) {
return self::$highlimit[$certainty];
}
}
+163
View File
@@ -0,0 +1,163 @@
<?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/>.
/**
* Defines the question behaviour type base class
*
* @package core
* @subpackage questionbehaviours
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* This class represents the type of behaviour, rather than the instance of the
* behaviour which control a particular question attempt.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_behaviour_type {
/**
* Certain behaviours are definitive of a way that questions can behave when
* attempted. For example deferredfeedback model, interactive model, etc.
* These are the options that should be listed in the user-interface, and
* for these behaviours this method should return true. Other behaviours are
* more implementation details, for example the informationitem behaviours,
* or a special subclass like interactive_adapted_for_my_qtype. These
* behaviours should return false.
* @return bool whether this is an archetypal behaviour.
*/
public function is_archetypal() {
return false;
}
/**
* Override this method if there are some display options that do not make
* sense 'during the attempt'.
* @return array of {@link question_display_options} field names, that are
* not relevant to this behaviour before a 'finish' action.
*/
public function get_unused_display_options() {
return array();
}
/**
* With this behaviour, is it possible that a question might finish as the student
* interacts with it, without a call to the {@link question_attempt::finish()} method?
* @return bool whether with this behaviour, questions may finish naturally.
*/
public function can_questions_finish_during_the_attempt() {
return false;
}
/**
* Adjust a random guess score for a question using this model. You have to
* do this without knowing details of the specific question, or which usage
* it is in.
* @param number $fraction the random guess score from the question type.
* @return number the adjusted fraction.
*/
public function adjust_random_guess_score($fraction) {
return $fraction;
}
/**
* Get summary information about a queston usage.
*
* Behaviours are not obliged to do anything here, but this is an opportunity
* to provide additional information that can be displayed in places like
* at the top of the quiz review page.
*
* In the return value, the array keys should be identifiers of the form
* qbehaviour_behaviourname_meaningfullkey. For qbehaviour_deferredcbm_highsummary.
* The values should be arrays with two items, title and content. Each of these
* should be either a string, or a renderable.
*
* To understand how to implement this method, look at the CBM behaviours,
* and their unit tests.
*
* @param question_usage_by_activity $quba the usage to provide summary data for.
* @return array as described above.
*/
public function summarise_usage(question_usage_by_activity $quba,
question_display_options $options) {
return array();
}
/**
* Does this question behaviour accept multiple submissions of responses within one attempt eg. multiple tries for the
* interactive or adaptive question behaviours.
*
* @return bool
*/
public function allows_multiple_submitted_responses() {
return false;
}
}
/**
* This class exists to allow behaviours that worked in Moodle 2.3 to continue
* to work. It implements the question_behaviour_type API for the other behaviour
* as much as possible in a backwards-compatible way.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_behaviour_type_fallback extends question_behaviour_type {
/** @var string the behaviour class name. */
protected $behaviourclass;
/**
* @param string $behaviourtype the type of behaviour we are providing a fallback for.
*/
public function __construct($behaviour) {
question_engine::load_behaviour_class($behaviour);
$this->behaviourclass = 'qbehaviour_' . $behaviour;
}
public function is_archetypal() {
return constant($this->behaviourclass . '::IS_ARCHETYPAL');
}
/**
* Override this method if there are some display options that do not make
* sense 'during the attempt'.
* @return array of {@link question_display_options} field names, that are
* not relevant to this behaviour before a 'finish' action.
*/
public function get_unused_display_options() {
return call_user_func(array($this->behaviourclass, 'get_unused_display_options'));
}
/**
* Adjust a random guess score for a question using this model. You have to
* do this without knowing details of the specific question, or which usage
* it is in.
* @param number $fraction the random guess score from the question type.
* @return number the adjusted fraction.
*/
public function adjust_random_guess_score($fraction) {
return call_user_func(array($this->behaviourclass, 'adjust_random_guess_score'),
$fraction);
}
}
@@ -0,0 +1,124 @@
<?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 that is like the deferred feedback model, but with
* certainty based marking. That is, in addition to the other controls, there are
* where the student can indicate how certain they are that their answer is right.
*
* @package qbehaviour
* @subpackage deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../deferredfeedback/behaviour.php');
/**
* Question behaviour for deferred feedback with certainty based marking.
*
* The student enters their response during the attempt, along with a certainty,
* that is, how sure they are that they are right, and it is saved. Later,
* when the whole attempt is finished, their answer is graded. Their degree
* of certainty affects their score.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredcbm extends qbehaviour_deferredfeedback {
const IS_ARCHETYPAL = true;
public function get_min_fraction() {
return question_cbm::adjust_fraction(0, question_cbm::HIGH);
}
public function get_max_fraction() {
return question_cbm::adjust_fraction(1, question_cbm::HIGH);
}
public function get_expected_data() {
if ($this->qa->get_state()->is_active()) {
return array('certainty' => PARAM_INT);
}
return parent::get_expected_data();
}
public function get_right_answer_summary() {
$summary = parent::get_right_answer_summary();
return question_cbm::summary_with_certainty($summary, question_cbm::HIGH);
}
public function get_correct_response() {
if ($this->qa->get_state()->is_active()) {
return array('certainty' => question_cbm::HIGH);
}
return array();
}
protected function get_our_resume_data() {
$lastcertainty = $this->qa->get_last_behaviour_var('certainty');
if ($lastcertainty) {
return array('-certainty' => $lastcertainty);
} else {
return array();
}
}
protected function is_same_response(question_attempt_step $pendingstep) {
return parent::is_same_response($pendingstep) &&
$this->qa->get_last_behaviour_var('certainty') ==
$pendingstep->get_behaviour_var('certainty');
}
protected function is_complete_response(question_attempt_step $pendingstep) {
return parent::is_complete_response($pendingstep) &&
$pendingstep->has_behaviour_var('certainty');
}
public function process_finish(question_attempt_pending_step $pendingstep) {
$status = parent::process_finish($pendingstep);
if ($status == question_attempt::KEEP) {
$fraction = $pendingstep->get_fraction();
if ($this->qa->get_last_step()->has_behaviour_var('certainty')) {
$certainty = $this->qa->get_last_step()->get_behaviour_var('certainty');
} else {
$certainty = question_cbm::default_certainty();
$pendingstep->set_behaviour_var('_assumedcertainty', $certainty);
}
if (!is_null($fraction)) {
$pendingstep->set_behaviour_var('_rawfraction', $fraction);
$pendingstep->set_fraction(question_cbm::adjust_fraction($fraction, $certainty));
}
$pendingstep->set_new_response_summary(
question_cbm::summary_with_certainty($pendingstep->get_new_response_summary(),
$this->qa->get_last_step()->get_behaviour_var('certainty')));
}
return $status;
}
public function summarise_action(question_attempt_step $step) {
$summary = parent::summarise_action($step);
if ($step->has_behaviour_var('certainty')) {
$summary = question_cbm::summary_with_certainty($summary,
$step->get_behaviour_var('certainty'));
}
return $summary;
}
}
@@ -0,0 +1,249 @@
<?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 deferred feedback with CBM behaviour.
*
* @package qbehaviour_deferredcbm
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../deferredfeedback/behaviourtype.php');
/**
* Question behaviour type information for deferred feedback with CBM behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredcbm_type extends qbehaviour_deferredfeedback_type {
public function adjust_random_guess_score($fraction) {
return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
}
public function summarise_usage(question_usage_by_activity $quba, question_display_options $options) {
global $OUTPUT;
$summarydata = parent::summarise_usage($quba, $options);
if ($options->marks < question_display_options::MARK_AND_MAX) {
return $summarydata;
}
// Prepare accumulators to hold the data we are about to collect.
$notansweredcount = 0;
$notansweredweight = 0;
$attemptcount = array(
question_cbm::HIGH => 0,
question_cbm::MED => 0,
question_cbm::LOW => 0,
);
$totalweight = array(
question_cbm::HIGH => 0,
question_cbm::MED => 0,
question_cbm::LOW => 0,
);
$totalrawscore = array(
question_cbm::HIGH => 0,
question_cbm::MED => 0,
question_cbm::LOW => 0,
);
$totalcbmscore = array(
question_cbm::HIGH => 0,
question_cbm::MED => 0,
question_cbm::LOW => 0,
);
// Loop through the data, and add it to the accumulators.
foreach ($quba->get_attempt_iterator() as $qa) {
if (strpos($qa->get_behaviour_name(), 'cbm') === false || $qa->get_max_mark() < 0.0000005) {
continue;
}
$gradedstep = $qa->get_last_step_with_behaviour_var('_rawfraction');
if (!$gradedstep->has_behaviour_var('_rawfraction')) {
$notansweredcount += 1;
$notansweredweight += $qa->get_max_mark();
continue;
}
$certainty = $qa->get_last_behaviour_var('certainty');
if (is_null($certainty) || $certainty == -1) {
// Certainty -1 has never been used in standard Moodle, but is
// used in Tony-Gardiner Medwin's patches to mean 'No idea' which
// we intend to implement: MDL-42077. In the mean time, avoid
// errors for people who have used TGM's patches.
$certainty = question_cbm::default_certainty();
}
$attemptcount[$certainty] += 1;
$totalweight[$certainty] += $qa->get_max_mark();
$totalrawscore[$certainty] += $qa->get_max_mark() * $gradedstep->get_behaviour_var('_rawfraction');
$totalcbmscore[$certainty] += $qa->get_mark();
}
// Hence compute some statistics.
$totalquestions = $notansweredcount + array_sum($attemptcount);
$grandtotalweight = $notansweredweight + array_sum($totalweight);
$accuracy = array_sum($totalrawscore) / $grandtotalweight;
$averagecbm = array_sum($totalcbmscore) / $grandtotalweight;
$cbmbonus = $this->calculate_bonus($averagecbm, $accuracy);
$accuracyandbonus = $accuracy + $cbmbonus;
// Add a note to explain the max mark.
$summarydata['qbehaviour_cbm_grade_explanation'] = array(
'title' => '',
'content' => html_writer::tag('i', get_string('cbmgradeexplanation', 'qbehaviour_deferredcbm')) .
$OUTPUT->help_icon('cbmgrades', 'qbehaviour_deferredcbm'),
);
// Now we can start generating some of the summary: overall values.
$summarydata['qbehaviour_cbm_entire_quiz_heading'] = array(
'title' => '',
'content' => html_writer::tag('h3',
get_string('forentirequiz', 'qbehaviour_deferredcbm', $totalquestions),
array('class' => 'qbehaviour_deferredcbm_summary_heading')),
);
$summarydata['qbehaviour_cbm_entire_quiz_cbm_average'] = array(
'title' => get_string('averagecbmmark', 'qbehaviour_deferredcbm'),
'content' => format_float($averagecbm, $options->markdp),
);
$summarydata['qbehaviour_cbm_entire_quiz_accuracy'] = array(
'title' => get_string('accuracy', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($accuracy, 1),
);
$summarydata['qbehaviour_cbm_entire_quiz_cbm_bonus'] = array(
'title' => get_string('cbmbonus', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($cbmbonus, 1),
);
$summarydata['qbehaviour_cbm_entire_quiz_accuracy_and_bonus'] = array(
'title' => get_string('accuracyandbonus', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($accuracyandbonus, 1),
);
if ($notansweredcount && array_sum($attemptcount) > 0) {
$totalquestions = array_sum($attemptcount);
$grandtotalweight = array_sum($totalweight);
$accuracy = array_sum($totalrawscore) / $grandtotalweight;
$averagecbm = array_sum($totalcbmscore) / $grandtotalweight;
$cbmbonus = $this->calculate_bonus($averagecbm, $accuracy);
$accuracyandbonus = $accuracy + $cbmbonus;
$summarydata['qbehaviour_cbm_answered_quiz_heading'] = array(
'title' => '',
'content' => html_writer::tag('h3',
get_string('foransweredquestions', 'qbehaviour_deferredcbm', $totalquestions),
array('class' => 'qbehaviour_deferredcbm_summary_heading')),
);
$summarydata['qbehaviour_cbm_answered_quiz_cbm_average'] = array(
'title' => get_string('averagecbmmark', 'qbehaviour_deferredcbm'),
'content' => format_float($averagecbm, $options->markdp),
);
$summarydata['qbehaviour_cbm_answered_quiz_accuracy'] = array(
'title' => get_string('accuracy', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($accuracy, 1),
);
$summarydata['qbehaviour_cbm_answered_quiz_cbm_bonus'] = array(
'title' => get_string('cbmbonus', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($cbmbonus, 1),
);
$summarydata['qbehaviour_cbm_answered_quiz_accuracy_and_bonus'] = array(
'title' => get_string('accuracyandbonus', 'qbehaviour_deferredcbm'),
'content' => $this->format_probability($accuracyandbonus, 1),
);
}
// Now per-certainty level values.
$summarydata['qbehaviour_cbm_judgement_heading'] = array(
'title' => '',
'content' => html_writer::tag('h3', get_string('breakdownbycertainty', 'qbehaviour_deferredcbm'),
array('class' => 'qbehaviour_deferredcbm_summary_heading')),
);
foreach ($attemptcount as $certainty => $count) {
$key = 'qbehaviour_cbm_judgement' . $certainty;
$title = question_cbm::get_short_string($certainty);
if ($count == 0) {
$summarydata[$key] = array(
'title' => $title,
'content' => get_string('noquestions', 'qbehaviour_deferredcbm'),
);
continue;
}
$lowerlimit = question_cbm::optimal_probablility_low($certainty);
$upperlimit = question_cbm::optimal_probablility_high($certainty);
$fraction = $totalrawscore[$certainty] / $totalweight[$certainty];
$a = new stdClass();
$a->responses = $count;
$a->idealrangelow = $this->format_probability($lowerlimit);
$a->idealrangehigh = $this->format_probability($upperlimit);
$a->fraction = html_writer::tag('span', $this->format_probability($fraction),
array('class' => 'qbehaviour_deferredcbm_actual_percentage'));
if ($fraction < $lowerlimit - 0.0000005) {
if ((pow($fraction - $lowerlimit, 2) * $count) > 0.5) { // Rough indicator of significance: t > 1.5 or 1.8.
$judgement = 'overconfident';
} else {
$judgement = 'slightlyoverconfident';
}
} else if ($fraction > $upperlimit + 0.0000005) {
if ((pow($fraction - $upperlimit, 2) * $count) > 0.5) {
$judgement = 'underconfident';
} else {
$judgement = 'slightlyunderconfident';
}
} else {
$judgement = 'judgementok';
}
$a->judgement = html_writer::tag('span', get_string($judgement, 'qbehaviour_deferredcbm'),
array('class' => 'qbehaviour_deferredcbm_' . $judgement));
$summarydata[$key] = array(
'title' => $title,
'content' => get_string('judgementsummary', 'qbehaviour_deferredcbm', $a),
);
}
return $summarydata;
}
protected function format_probability($probability, $dp = 0) {
return format_float($probability * 100, $dp) . '%';
}
public function calculate_bonus($total, $accuracy) {
$expectedforaccuracy = max(
$accuracy * question_cbm::adjust_fraction(1, question_cbm::LOW) +
(1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::LOW),
$accuracy * question_cbm::adjust_fraction(1, question_cbm::MED) +
(1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::MED),
$accuracy * question_cbm::adjust_fraction(1, question_cbm::HIGH) +
(1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::HIGH)
);
// The constant 0.1 here is determinted empirically from looking at lots
// for CBM quiz results. See www.ucl.ac.uk/~ucgbarg/tea/IUPS_2013a.pdf.
// It approximately maximises the reliability of accuracy + bonus.
return 0.1 * ($total - $expectedforaccuracy);
}
}
@@ -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_deferredcbm.
*
* @package qbehaviour_deferredcbm
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_deferredcbm\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_deferredcbm 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,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/>.
/**
* Strings for component 'qbehaviour_deferredcbm', language 'en'.
*
* @package qbehaviour
* @subpackage deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['accuracy'] = 'Accuracy';
$string['accuracyandbonus'] = 'Accuracy + Bonus';
$string['assumingcertainty'] = 'You did not select a certainty. Assuming: {$a}.';
$string['averagecbmmark'] = 'Average CBM mark';
$string['basemark'] = 'Base mark {$a}';
$string['breakdownbycertainty'] = 'Break-down by certainty';
$string['cbmbonus'] = 'CBM bonus';
$string['cbmmark'] = 'CBM mark {$a}';
$string['cbmgradeexplanation'] = 'For CBM, the grade above is shown relative to the maximum for all correct at C=1.';
$string['cbmgrades'] = 'CBM grades';
$string['cbmgrades_help'] = 'With Certainty Based Marking (CBM) getting every question correct with C=1 (low certainty) gives a grade of 100%. Grades may be as high as 300% if every question is correct with C=3 (high certainty). Misconceptions (confident wrong responses) lower grades much more than wrong responses that are acknowledged to be uncertain. This may even lead to negative overall grades.
**Accuracy** is the % correct ignoring certainty but weighted for the maximum mark of each question. Successfully distinguishing more and less reliable responses gives a better grade than selecting the same certainty for each question. This is reflected in the **CBM Bonus**. **Accuracy** + **CBM Bonus** is a better measure of knowledge than **Accuracy**. Misconceptions can lead to a negative bonus, a warning to look carefully at what is and is not known.';
$string['cbmgrades_link'] = 'qbehaviour/deferredcbm/certaintygrade';
$string['certainty'] = 'Certainty';
$string['certainty_help'] = 'Certainty-based marking requires you to indicate how reliable you think your answer is. The available levels are:
Certainty level | C=1 (Unsure) | C=2 (Mid) | C=3 (Quite sure)
------------------- | ------------ | --------- | ----------------
Mark if correct | 1 | 2 | 3
Mark if wrong | 0 | -2 | -6
Probability correct | <67% | 67-80% | >80%
Best marks are gained by acknowledging uncertainty. For example, if you think there is more than a 1 in 3 chance of being wrong, you should enter C=1 and avoid the risk of a negative mark.
';
$string['certainty_link'] = 'qbehaviour/deferredcbm/certainty';
$string['certainty-1'] = 'No Idea';
$string['certainty1'] = 'C=1 (Unsure: <67%)';
$string['certainty2'] = 'C=2 (Mid: >67%)';
$string['certainty3'] = 'C=3 (Quite sure: >80%)';
$string['certaintyshort-1'] = 'No Idea';
$string['certaintyshort1'] = 'C=1';
$string['certaintyshort2'] = 'C=2';
$string['certaintyshort3'] = 'C=3';
$string['dontknow'] = 'No idea';
$string['foransweredquestions'] = 'Results for just the {$a} answered questions';
$string['forentirequiz'] = 'Results for the whole quiz ({$a} questions)';
$string['judgementok'] = 'OK';
$string['judgementsummary'] = 'Responses: {$a->responses}. Accuracy: {$a->fraction}. (Optimal range {$a->idealrangelow} to {$a->idealrangehigh}). You were {$a->judgement} using this certainty level.';
$string['howcertainareyou'] = 'Certainty{$a->help}: {$a->choices}';
$string['noquestions'] = 'No responses';
$string['overconfident'] = 'over-confident';
$string['pluginname'] = 'Deferred feedback with CBM';
$string['privacy:metadata'] = 'The Deferred feedback with CBM question behaviour plugin does not store any personal data.';
$string['slightlyoverconfident'] = 'a bit over-confident';
$string['slightlyunderconfident'] = 'a bit under-confident';
$string['underconfident'] = 'under-confident';
$string['weightx'] = 'Weight {$a}';
+108
View File
@@ -0,0 +1,108 @@
<?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/>.
/**
* Defines the renderer for the deferred feedback with certainty based marking
* behaviour.
*
* @package qbehaviour
* @subpackage deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the deferred
* feedback with certainty based marking behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredcbm_renderer extends qbehaviour_renderer {
protected function certainty_choices($controlname, $selected, $readonly) {
$attributes = array(
'type' => 'radio',
'name' => $controlname,
);
if ($readonly) {
$attributes['disabled'] = 'disabled';
}
$choices = '';
foreach (question_cbm::$certainties as $certainty) {
$id = $controlname . $certainty;
$attributes['id'] = $id;
$attributes['value'] = $certainty;
if ($selected == $certainty) {
$attributes['checked'] = 'checked';
} else {
unset($attributes['checked']);
}
$choices .= ' ' .
html_writer::tag('label', html_writer::empty_tag('input', $attributes) .
question_cbm::get_string($certainty), array('for' => $id));
}
return $choices;
}
public function controls(question_attempt $qa, question_display_options $options) {
$a = new stdClass();
$a->help = $this->output->help_icon('certainty', 'qbehaviour_deferredcbm');
$a->choices = $this->certainty_choices($qa->get_behaviour_field_name('certainty'),
$qa->get_last_behaviour_var('certainty'), $options->readonly);
return html_writer::tag('div', get_string('howcertainareyou', 'qbehaviour_deferredcbm', $a),
array('class' => 'certaintychoices'));
}
public function feedback(question_attempt $qa, question_display_options $options) {
if (!$options->feedback) {
return '';
}
if ($qa->get_state() == question_state::$gaveup || $qa->get_state() ==
question_state::$mangaveup) {
return '';
}
$feedback = '';
if (!$qa->get_last_behaviour_var('certainty') &&
$qa->get_last_behaviour_var('_assumedcertainty')) {
$feedback .= html_writer::tag('p',
get_string('assumingcertainty', 'qbehaviour_deferredcbm',
question_cbm::get_string($qa->get_last_behaviour_var('_assumedcertainty'))));
}
return $feedback;
}
public function marked_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
question_display_options $options) {
return get_string('weightx', 'qbehaviour_deferredcbm', $qa->format_fraction_as_mark(
question_cbm::adjust_fraction(1, question_cbm::default_certainty()),
$options->markdp));
}
public function mark_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
question_display_options $options) {
return get_string('cbmmark', 'qbehaviour_deferredcbm', $qa->format_mark($options->markdp)) .
'<br>' . $this->marked_out_of_max($qa, $qoutput, $options);
}
}
+32
View File
@@ -0,0 +1,32 @@
.qbehaviour_deferredcbm_slightlyunderconfident,
.qbehaviour_deferredcbm_slightlyoverconfident {
font-weight: bold;
color: #600;
}
.qbehaviour_deferredcbm_underconfident,
.qbehaviour_deferredcbm_overconfident {
font-weight: bold;
color: #c00;
}
.qbehaviour_deferredcbm_judgementok {
font-weight: bold;
color: #080;
}
.qbehaviour_deferredcbm_actual_percentage {
font-weight: bold;
}
.qbehaviour_deferredcbm_summary_heading {
margin: 0;
}
.que.deferredcbm .certaintychoices input[type="radio"] {
margin-left: 0.5em;
}
.que.deferredcbm .certaintychoices label {
white-space: nowrap;
}
@@ -0,0 +1,142 @@
<?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_deferredcbm;
use question_display_options;
use question_engine;
use question_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
require_once(__DIR__ . '/../../../engine/tests/helpers.php');
/**
* Unit tests for the deferred feedback with certainty base marking behaviour.
*
* @package qbehaviour_deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behaviour_type_test extends \qbehaviour_walkthrough_test_base {
/** @var qbehaviour_deferredcbm_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('deferredcbm');
}
public function test_is_archetypal(): void {
$this->assertTrue($this->behaviourtype->is_archetypal());
}
public function test_get_unused_display_options(): void {
$this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
$this->behaviourtype->get_unused_display_options());
}
public function test_can_questions_finish_during_the_attempt(): void {
$this->assertFalse($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));
}
public function test_summarise_usage_max_mark_1(): void {
// Create a usage comprising 3 true-false questions.
$this->quba->set_preferred_behaviour('deferredcbm');
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 3);
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 3);
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 3);
$this->quba->start_all_questions();
// Process responses right, high certainty; right, med certainty; wrong, med certainty.
$this->quba->process_action(1, array('answer' => 1, '-certainty' => 3));
$this->quba->process_action(2, array('answer' => 1, '-certainty' => 2));
$this->quba->process_action(3, array('answer' => 0, '-certainty' => 2));
$this->quba->finish_all_questions();
// Get the summary.
$summarydata = $this->quba->get_summary_information(new question_display_options());
// Verify.
$this->assertStringContainsString(get_string('breakdownbycertainty', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement_heading']['content']);
$this->assertStringContainsString('100%',
$summarydata['qbehaviour_cbm_judgement3']['content']);
$this->assertStringContainsString(get_string('judgementok', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement3']['content']);
$this->assertStringContainsString('50%',
$summarydata['qbehaviour_cbm_judgement2']['content']);
$this->assertStringContainsString(get_string('slightlyoverconfident', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement2']['content']);
$this->assertStringContainsString(get_string('noquestions', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement1']['content']);
}
public function test_summarise_usage_max_mark_3(): void {
// Create a usage comprising 3 true-false questions.
$this->quba->set_preferred_behaviour('deferredcbm');
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 1);
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 1);
$this->quba->add_question(\test_question_maker::make_question('truefalse', 'true'), 1);
$this->quba->start_all_questions();
// Process responses right, high certainty; right, med certainty; wrong, med certainty.
$this->quba->process_action(1, array('answer' => 1, '-certainty' => 3));
$this->quba->process_action(2, array('answer' => 1, '-certainty' => 2));
$this->quba->process_action(3, array('answer' => 0, '-certainty' => 2));
$this->quba->finish_all_questions();
// Get the summary.
$summarydata = $this->quba->get_summary_information(new question_display_options());
// Verify.
$this->assertStringContainsString(get_string('breakdownbycertainty', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement_heading']['content']);
$this->assertStringContainsString('100%',
$summarydata['qbehaviour_cbm_judgement3']['content']);
$this->assertStringContainsString(get_string('judgementok', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement3']['content']);
$this->assertStringContainsString('50%',
$summarydata['qbehaviour_cbm_judgement2']['content']);
$this->assertStringContainsString(get_string('slightlyoverconfident', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement2']['content']);
$this->assertStringContainsString(get_string('noquestions', 'qbehaviour_deferredcbm'),
$summarydata['qbehaviour_cbm_judgement1']['content']);
}
public function test_calculate_bonus(): void {
$this->assertEqualsWithDelta(0.05, $this->behaviourtype->calculate_bonus(1, 1 / 2), question_testcase::GRADE_DELTA);
$this->assertEqualsWithDelta(-0.01, $this->behaviourtype->calculate_bonus(2, 9 / 10), question_testcase::GRADE_DELTA);
$this->assertEqualsWithDelta(0, $this->behaviourtype->calculate_bonus(3, 1), question_testcase::GRADE_DELTA);
}
}
@@ -0,0 +1,49 @@
<?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_deferredcbm;
use question_cbm;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
/**
* Unit tests for the deferred feedback with certainty base marking behaviour.
*
* @package qbehaviour_deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_cbm_test extends \basic_testcase {
public function test_adjust_fraction(): void {
$this->assertEqualsWithDelta( 1, question_cbm::adjust_fraction( 1, question_cbm::LOW), 0.0000001);
$this->assertEqualsWithDelta( 2, question_cbm::adjust_fraction( 1, question_cbm::MED), 0.0000001);
$this->assertEqualsWithDelta( 3, question_cbm::adjust_fraction( 1, question_cbm::HIGH), 0.0000001);
$this->assertEqualsWithDelta( 0, question_cbm::adjust_fraction( 0, question_cbm::LOW), 0.0000001);
$this->assertEqualsWithDelta(-2, question_cbm::adjust_fraction( 0, question_cbm::MED), 0.0000001);
$this->assertEqualsWithDelta(-6, question_cbm::adjust_fraction( 0, question_cbm::HIGH), 0.0000001);
$this->assertEqualsWithDelta( 0.5, question_cbm::adjust_fraction( 0.5, question_cbm::LOW), 0.0000001);
$this->assertEqualsWithDelta( 1, question_cbm::adjust_fraction( 0.5, question_cbm::MED), 0.0000001);
$this->assertEqualsWithDelta( 1.5, question_cbm::adjust_fraction( 0.5, question_cbm::HIGH), 0.0000001);
$this->assertEqualsWithDelta( 0, question_cbm::adjust_fraction(-0.25, question_cbm::LOW), 0.0000001);
$this->assertEqualsWithDelta(-2, question_cbm::adjust_fraction(-0.25, question_cbm::MED), 0.0000001);
$this->assertEqualsWithDelta(-6, question_cbm::adjust_fraction(-0.25, question_cbm::HIGH), 0.0000001);
}
}
@@ -0,0 +1,277 @@
<?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_deferredcbm;
use question_cbm;
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 deferred feedback with certainty base marking behaviour.
*
* @package qbehaviour_deferredcbm
* @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_deferred_cbm_truefalse_high_certainty(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'deferredcbm', 2);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_question_text_expectation($tf),
$this->get_contains_tf_true_radio_expectation(true, false),
$this->get_contains_tf_false_radio_expectation(true, false),
$this->get_contains_cbm_radio_expectation(1, true, false),
$this->get_contains_cbm_radio_expectation(2, true, false),
$this->get_contains_cbm_radio_expectation(3, true, false),
$this->get_does_not_contain_feedback_expectation());
// Process the data extracted for this question.
$this->process_submission(array('answer' => 1, '-certainty' => 3));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('answersaved', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_tf_true_radio_expectation(true, true),
$this->get_contains_cbm_radio_expectation(3, true, true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Process the same data again, check it does not create a new step.
$numsteps = $this->get_step_count();
$this->process_submission(array('answer' => 1, '-certainty' => 3));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
$this->process_submission(array('answer' => 1, '-certainty' => 1));
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$complete);
// Change back, check it creates a new step.
$this->process_submission(array('answer' => 1, '-certainty' => 3));
$this->check_step_count($numsteps + 2);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(6);
$this->check_current_output(
$this->get_contains_tf_true_radio_expectation(false, true),
$this->get_contains_cbm_radio_expectation(3, false, true),
$this->get_contains_correct_expectation());
// Process a manual comment.
$this->manual_grade('Not good enough!', 5, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrright);
$this->check_current_mark(5);
$this->check_current_output(new \question_pattern_expectation('/' .
preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$tf->rightanswer = false;
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$mangrright);
$this->check_current_mark(5);
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta(-6, $autogradedstep->get_fraction(), 0.0000001);
}
public function test_deferred_cbm_truefalse_low_certainty(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'deferredcbm', 2);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_cbm_radio_expectation(1, true, false),
$this->get_does_not_contain_feedback_expectation());
// Submit ansewer with low certainty.
$this->process_submission(array('answer' => 1, '-certainty' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('answersaved', 'question');
$this->check_current_mark(null);
$this->check_current_output($this->get_does_not_contain_correctness_expectation(),
$this->get_contains_cbm_radio_expectation(1, true, true),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output($this->get_contains_correct_expectation(),
$this->get_contains_cbm_radio_expectation(1, false, true));
$this->assertEquals(get_string('true', 'qtype_truefalse') . ' [' .
question_cbm::get_short_string(question_cbm::LOW) . ']',
$this->quba->get_response_summary($this->slot));
}
public function test_deferred_cbm_truefalse_default_certainty(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'deferredcbm', 2);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_cbm_radio_expectation(1, true, false),
$this->get_does_not_contain_feedback_expectation());
// Submit ansewer with low certainty and finish the attempt.
$this->process_submission(array('answer' => 1));
$this->quba->finish_all_questions();
// Verify.
$qa = $this->quba->get_question_attempt($this->slot);
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output($this->get_contains_correct_expectation(),
$this->get_contains_cbm_radio_expectation(1, false, false),
new \question_pattern_expectation('/' . preg_quote(
get_string('assumingcertainty', 'qbehaviour_deferredcbm',
question_cbm::get_string(
$qa->get_last_behaviour_var('_assumedcertainty'))), '/') . '/'));
$this->assertEquals(get_string('true', 'qtype_truefalse'),
$this->quba->get_response_summary($this->slot));
}
public function test_deferredcbm_resume_multichoice_single(): void {
// Create a multiple-choice question.
$mc = \test_question_maker::make_a_multichoice_single_question();
// Attempt it getting it wrong.
$this->start_attempt_at_question($mc, 'deferredcbm', 1);
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
$this->process_submission(array('answer' => $wrongindex, '-certainty' => 2));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-2);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_cbm_radio_expectation(2, false, true),
$this->get_contains_incorrect_expectation());
$this->assertEquals('A [' . question_cbm::get_short_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertMatchesRegularExpression('/' . preg_quote($mc->questiontext, '/') . '/',
$this->quba->get_question_summary($this->slot));
$this->assertMatchesRegularExpression(
'/(B|C) \[' . preg_quote(question_cbm::get_short_string(question_cbm::MED), '/') . '\]/',
$this->quba->get_response_summary($this->slot));
// Save the old attempt.
$oldqa = $this->quba->get_question_attempt($this->slot);
// Reinitialise.
$this->setUp();
$this->quba->set_preferred_behaviour('deferredcbm');
$this->slot = $this->quba->add_question($mc, 1);
$this->quba->start_question_based_on($this->slot, $oldqa);
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('notchanged', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_contains_cbm_radio_expectation(2, true, true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_correctness_expectation());
$this->assertEquals('A [' . question_cbm::get_short_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertMatchesRegularExpression('/' . preg_quote($mc->questiontext, '/') . '/',
$this->quba->get_question_summary($this->slot));
$this->assertNull($this->quba->get_response_summary($this->slot));
// Now get it right.
$this->process_submission(array('answer' => $rightindex, '-certainty' => 3));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_cbm_radio_expectation(question_cbm::HIGH, false, true),
$this->get_contains_correct_expectation());
$this->assertMatchesRegularExpression(
'/(A) \[' . preg_quote(question_cbm::get_short_string(question_cbm::HIGH), '/') . '\]/',
$this->quba->get_response_summary($this->slot));
}
public function test_deferred_cbm_truefalse_no_certainty_feedback_when_not_answered(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'deferredcbm', 2);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_cbm_radio_expectation(1, true, false),
$this->get_does_not_contain_feedback_expectation());
// Finish without answering.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
new \question_no_pattern_expectation('/class=\"im-feedback/'));
}
}
@@ -0,0 +1,36 @@
<?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 deferredcbm
* @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_deferredcbm';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->dependencies = [
'qbehaviour_deferredfeedback' => 2024041600,
];
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,116 @@
<?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 for the case when the student's answer is just
* saved until they submit the whole attempt, and then it is graded.
*
* @package qbehaviour
* @subpackage deferredfeedback
* @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 deferred feedback.
*
* The student enters their response during the attempt, and it is saved. Later,
* when the whole attempt is finished, their answer is graded.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredfeedback extends question_behaviour_with_save {
public function is_compatible_question(question_definition $question) {
return $question instanceof question_automatically_gradable;
}
public function get_min_fraction() {
return $this->question->get_min_fraction();
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
} else {
return $this->process_save($pendingstep);
}
}
/*
* Like the parent method, except that when a respones is gradable, but not
* completely, we move it to the invalid state.
*
* TODO refactor, to remove the duplication.
*/
public function process_save(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
} else if (!$this->qa->get_state()->is_active()) {
throw new coding_exception('Question is not active, cannot process_actions.');
}
if ($this->is_same_response($pendingstep)) {
return question_attempt::DISCARD;
}
if ($this->is_complete_response($pendingstep)) {
$pendingstep->set_state(question_state::$complete);
} else if ($this->question->is_gradable_response($pendingstep->get_qt_data())) {
$pendingstep->set_state(question_state::$invalid);
} else {
$pendingstep->set_state(question_state::$todo);
}
return question_attempt::KEEP;
}
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 {
return $this->summarise_save($step);
}
}
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_step()->get_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($fraction);
$pendingstep->set_state($state);
}
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
return question_attempt::KEEP;
}
}
@@ -0,0 +1,44 @@
<?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 deferred feedback behaviour.
*
* @package qbehaviour_deferredfeedback
* @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 deferred feedback behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredfeedback_type extends question_behaviour_type {
public function is_archetypal() {
return true;
}
public function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');
}
}
@@ -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_deferredfeedback.
*
* @package qbehaviour_deferredfeedback
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_deferredfeedback\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_deferredfeedback 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,27 @@
<?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_deferredfeedback', language 'en'.
*
* @package qbehaviour
* @subpackage deferredfeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Deferred feedback';
$string['privacy:metadata'] = 'The Deferred feedback behaviour plugin does not store any personal data.';
@@ -0,0 +1,38 @@
<?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/>.
/**
* Defines the renderer for the deferred feedback behaviour.
*
* @package qbehaviour
* @subpackage deferredfeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the deferred
* feedback behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_deferredfeedback_renderer extends qbehaviour_renderer {
}
@@ -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_deferredfeedback;
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 deferred feedback behaviour type class.
*
* @package qbehaviour_deferredfeedback
* @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 \qbehaviour_walkthrough_test_base {
/** @var qbehaviour_deferredfeedback_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('deferredfeedback');
}
public function test_is_archetypal(): void {
$this->assertTrue($this->behaviourtype->is_archetypal());
}
public function test_can_questions_finish_during_the_attempt(): void {
$this->assertFalse($this->behaviourtype->can_questions_finish_during_the_attempt());
}
public function test_get_unused_display_options(): void {
$this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
$this->behaviourtype->get_unused_display_options());
}
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,303 @@
<?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_deferredfeedback;
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 deferred feedback behaviour.
*
* @package qbehaviour_deferredfeedback
* @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_deferredfeedback_feedback_truefalse(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'deferredfeedback', 2);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($tf),
$this->get_does_not_contain_feedback_expectation());
$this->assertEquals(get_string('true', 'qtype_truefalse'),
$this->quba->get_right_answer_summary($this->slot));
$this->assertMatchesRegularExpression('/' . preg_quote($tf->questiontext, '/') . '/',
$this->quba->get_question_summary($this->slot));
$this->assertNull($this->quba->get_response_summary($this->slot));
// Process a true answer and check the expected result.
$this->process_submission(array('answer' => 1));
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('answersaved', 'question');
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_tf_true_radio_expectation(true, true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Process the same data again, check it does not create a new step.
$numsteps = $this->get_step_count();
$this->process_submission(array('answer' => 1));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
$this->process_submission(array('answer' => 0));
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$complete);
// Change back, check it creates a new step.
$this->process_submission(array('answer' => 1));
$this->check_step_count($numsteps + 2);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output($this->get_contains_correct_expectation(),
$this->get_contains_tf_true_radio_expectation(false, true),
new \question_pattern_expectation('/class="r0 correct"/'));
$this->assertEquals(get_string('true', 'qtype_truefalse'),
$this->quba->get_response_summary($this->slot));
// Process a manual comment.
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$tf->rightanswer = false;
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), 0, 0.0000001);
}
public function test_deferredfeedback_feedback_multichoice_single(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'deferredfeedback', 3);
// Start a deferred feedback attempt and add the question to it.
$rightindex = $this->get_mc_right_answer_index($mc);
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$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_does_not_contain_feedback_expectation());
// Process the data extracted for this question.
$this->process_submission(array('answer' => $rightindex));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('answersaved', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($rightindex, true, true),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_correct_expectation());
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
$mc->answers[14]->fraction = 1;
$this->quba->regrade_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-1);
$this->check_current_output(
$this->get_contains_incorrect_expectation());
}
public function test_deferredfeedback_resume_multichoice_single(): void {
// Create a multiple-choice question.
$mc = \test_question_maker::make_a_multichoice_single_question();
// Attempt it getting it wrong.
$this->start_attempt_at_question($mc, 'deferredfeedback', 3);
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->process_submission(array('answer' => $wrongindex));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-1);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_incorrect_expectation());
// Save the old attempt.
$oldqa = $this->quba->get_question_attempt($this->slot);
// Reinitialise.
$this->setUp();
$this->quba->set_preferred_behaviour('deferredfeedback');
$this->slot = $this->quba->add_question($mc, 3);
$this->quba->start_question_based_on($this->slot, $oldqa);
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('notchanged', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_correctness_expectation());
// Now get it right.
$this->process_submission(array('answer' => $rightindex));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_correct_expectation());
}
public function test_deferredfeedback_resume_multichoice_single_emptyanswer_first(): void {
// Create a multiple-choice question.
$mc = \test_question_maker::make_a_multichoice_single_question();
// Attempt it and submit empty.
$this->start_attempt_at_question($mc, 'deferredfeedback', 3);
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->process_submission(array('-submit' => 1));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation(0, false, false),
$this->get_contains_mc_radio_expectation(1, false, false),
$this->get_contains_mc_radio_expectation(2, false, false),
$this->get_contains_general_feedback_expectation($mc));
// Save the old attempt.
$oldqa = $this->quba->get_question_attempt($this->slot);
// Reinitialise.
$this->setUp();
$this->quba->set_preferred_behaviour('deferredfeedback');
$this->slot = $this->quba->add_question($mc, 3);
$this->quba->start_question_based_on($this->slot, $oldqa);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_output_contains_lang_string('notyetanswered', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$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_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_correctness_expectation());
// Now get it wrong.
$this->process_submission(array('answer' => $wrongindex));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-1);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, false, true),
$this->get_contains_incorrect_expectation());
// Save the old attempt.
$oldqa = $this->quba->get_question_attempt($this->slot);
// Reinitialise.
$this->setUp();
$this->quba->set_preferred_behaviour('deferredfeedback');
$this->slot = $this->quba->add_question($mc, 3);
$this->quba->start_question_based_on($this->slot, $oldqa);
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_output_contains_lang_string('notchanged', 'question');
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_correctness_expectation());
// Now get it right.
$this->process_submission(array('answer' => $rightindex));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_contains_mc_radio_expectation($rightindex, false, true),
$this->get_contains_correct_expectation());
}
}
@@ -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 deferredfeedback
* @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_deferredfeedback';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,153 @@
<?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, with certainty based marking.
*
* @package qbehaviour
* @subpackage immediatecbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../immediatefeedback/behaviour.php');
/**
* Question behaviour for immediate feedback with CBM.
*
* Each question has a submit button next to it along with some radio buttons
* to input a certainty, that is, how sure they are that they are right.
* The student can submit their answer at any time for immediate feedback.
* Once the qustion is submitted, it is not possible for the student to change
* their answer any more. The student's degree of certainty affects their score.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_immediatecbm extends qbehaviour_immediatefeedback {
public function get_min_fraction() {
return question_cbm::adjust_fraction(parent::get_min_fraction(), question_cbm::HIGH);
}
public function get_max_fraction() {
return question_cbm::adjust_fraction(parent::get_max_fraction(), question_cbm::HIGH);
}
public function get_expected_data() {
if ($this->qa->get_state()->is_active()) {
return array(
'submit' => PARAM_BOOL,
'certainty' => PARAM_INT,
);
}
return parent::get_expected_data();
}
public function get_right_answer_summary() {
$summary = parent::get_right_answer_summary();
return question_cbm::summary_with_certainty($summary, question_cbm::HIGH);
}
public function get_correct_response() {
if ($this->qa->get_state()->is_active()) {
return array('certainty' => question_cbm::HIGH);
}
return array();
}
protected function get_our_resume_data() {
$lastcertainty = $this->qa->get_last_behaviour_var('certainty');
if ($lastcertainty) {
return array('-certainty' => $lastcertainty);
} else {
return array();
}
}
protected function is_same_response(question_attempt_step $pendingstep) {
return parent::is_same_response($pendingstep) &&
$this->qa->get_last_behaviour_var('certainty') ==
$pendingstep->get_behaviour_var('certainty');
}
protected function is_complete_response(question_attempt_step $pendingstep) {
return parent::is_complete_response($pendingstep) &&
$pendingstep->has_behaviour_var('certainty');
}
public function process_submit(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
if (!$this->question->is_gradable_response($pendingstep->get_qt_data()) ||
!$pendingstep->has_behaviour_var('certainty')) {
$pendingstep->set_state(question_state::$invalid);
return question_attempt::KEEP;
}
return $this->do_grading($pendingstep, $pendingstep);
}
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
$laststep = $this->qa->get_last_step();
return $this->do_grading($laststep, $pendingstep);
}
protected function do_grading(question_attempt_step $responsesstep,
question_attempt_pending_step $pendingstep) {
if (!$this->question->is_gradable_response($responsesstep->get_qt_data())) {
$pendingstep->set_state(question_state::$gaveup);
} else {
$response = $responsesstep->get_qt_data();
list($fraction, $state) = $this->question->grade_response($response);
if ($responsesstep->has_behaviour_var('certainty')) {
$certainty = $responsesstep->get_behaviour_var('certainty');
} else {
$certainty = question_cbm::default_certainty();
$pendingstep->set_behaviour_var('_assumedcertainty', $certainty);
}
$pendingstep->set_behaviour_var('_rawfraction', $fraction);
$pendingstep->set_fraction(question_cbm::adjust_fraction($fraction, $certainty));
$pendingstep->set_state($state);
$pendingstep->set_new_response_summary(question_cbm::summary_with_certainty(
$this->question->summarise_response($response),
$responsesstep->get_behaviour_var('certainty')));
}
return question_attempt::KEEP;
}
public function summarise_action(question_attempt_step $step) {
$summary = parent::summarise_action($step);
if ($step->has_behaviour_var('certainty')) {
$summary = question_cbm::summary_with_certainty($summary,
$step->get_behaviour_var('certainty'));
}
return $summary;
}
}
@@ -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/>.
/**
* Question behaviour type for immediate feedback with CBM behaviour.
*
* @package qbehaviour_adaptive
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../deferredcbm/behaviourtype.php');
/**
* Question behaviour type information for immediate feedback with CBM.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_immediatecbm_type extends qbehaviour_deferredcbm_type {
public function get_unused_display_options() {
return array();
}
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_immediatecbm.
*
* @package qbehaviour_immediatecbm
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_immediatecbm\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_immediatecbm 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,28 @@
<?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_immediatecbm', language 'en'.
*
* @package qbehaviour
* @subpackage immediatecbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pleaseselectacertainty'] = 'Please select a certainty.';
$string['pluginname'] = 'Immediate feedback with CBM';
$string['privacy:metadata'] = 'The Immediate feedback with CBM question behaviour plugin does not store any personal data.';
@@ -0,0 +1,51 @@
<?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/>.
/**
* Defines the renderer for the immediate feedback with CBM behaviour.
*
* @package qbehaviour
* @subpackage immediatecbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../deferredcbm/renderer.php');
/**
* Renderer for outputting parts of a question belonging to the immediate
* feedback with CBM behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_immediatecbm_renderer extends qbehaviour_deferredcbm_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
$output = parent::controls($qa, $options);
if ($qa->get_state() == question_state::$invalid &&
!$qa->get_last_step()->has_behaviour_var('certainty')) {
$output .= html_writer::tag('div',
get_string('pleaseselectacertainty', 'qbehaviour_immediatecbm'),
array('class' => 'validationerror'));
}
$output .= $this->submit_button($qa, $options);
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_immediatecbm;
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 immediate feedback with CBM behaviour type class.
*
* @package qbehaviour_immediatecbm
* @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 \qbehaviour_walkthrough_test_base {
/** @var qbehaviour_immediatecbm_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('immediatecbm');
}
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,287 @@
<?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_immediatecbm;
use question_cbm;
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 immediate cbm behaviour.
*
* @package qbehaviour_immediatecbm
* @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_immediatecbm_feedback_multichoice_right(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatecbm');
$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_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->assertEquals('A [' . question_cbm::get_short_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertMatchesRegularExpression('/' . preg_quote($mc->questiontext, '/') . '/',
$this->quba->get_question_summary($this->slot));
$this->assertNull($this->quba->get_response_summary($this->slot));
// Save the wrong answer.
$this->process_submission(array('answer' => $wrongindex, '-certainty' => 1));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
// Submit the right answer.
$this->process_submission(
array('answer' => $rightindex, '-certainty' => 2, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output(
$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->assertEquals('A [' . question_cbm::get_short_string(2) . ']',
$this->quba->get_response_summary($this->slot));
$numsteps = $this->get_step_count();
// Finish the attempt - should not need to add a new state.
$this->quba->finish_all_questions();
// Verify.
$this->assertEquals($numsteps, $this->get_step_count());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
$this->check_current_output(
$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());
// 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_partcorrect_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
$mc->answers[15]->fraction = 1;
$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_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), -2, 0.0000001);
}
public function test_immediatecbm_feedback_multichoice_try_to_submit_blank(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatecbm');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
// Submit nothing.
$this->process_submission(array('-submit' => 1));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$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_correctness_expectation(),
$this->get_contains_validation_error_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation(0, false, false),
$this->get_contains_mc_radio_expectation(1, false, false),
$this->get_contains_mc_radio_expectation(2, false, false));
// 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_partcorrect_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
}
public function test_immediatecbm_feedback_shortanswer_try_to_submit_no_certainty(): void {
// Create a short answer question with correct answer true.
$sa = \test_question_maker::make_question('shortanswer');
$this->start_attempt_at_question($sa, 'immediatecbm');
// 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());
// Submit with certainty missing.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_validation_error_expectation());
// Now get it right.
$this->process_submission(array('-submit' => 1, 'answer' => 'frog', '-certainty' => 3));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
$this->get_does_not_contain_validation_error_expectation());
}
public function test_immediatecbm_feedback_multichoice_wrong_on_finish(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatecbm');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
// Save the wrong answer.
$this->process_submission(array('answer' => $wrongindex, '-certainty' => 3));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-6);
$this->check_current_output(
$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());
}
public function test_immediatecbm_cbm_truefalse_no_certainty_feedback_when_not_answered(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'immediatecbm', 2);
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_contains_cbm_radio_expectation(1, true, false),
$this->get_does_not_contain_feedback_expectation());
// Finish without answering.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
new \question_no_pattern_expectation('/class=\"im-feedback/'));
}
}
@@ -0,0 +1,37 @@
<?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 immediatecbm
* @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_immediatecbm';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->dependencies = [
'qbehaviour_immediatefeedback' => 2024041600,
'qbehaviour_deferredcbm' => 2024041600,
];
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,147 @@
<?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 immediatefeedback
* @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 immediate feedback.
*
* Each question has a submit button next to it which the student can use to
* submit it. Once the qustion 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_immediatefeedback extends question_behaviour_with_save {
const IS_ARCHETYPAL = true;
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_min_fraction() {
return $this->question->get_min_fraction();
}
public function get_expected_data() {
if ($this->qa->get_state()->is_active()) {
return array(
'submit' => PARAM_BOOL,
);
}
return parent::get_expected_data();
}
public function get_state_string($showcorrectness) {
$state = $this->qa->get_state();
if ($state == question_state::$todo) {
return get_string('notcomplete', 'qbehaviour_immediatefeedback');
} else {
return parent::get_state_string($showcorrectness);
}
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('submit')) {
return $this->process_submit($pendingstep);
} else if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($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('submit')) {
return $this->summarise_submit($step);
} else {
return $this->summarise_save($step);
}
}
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 {
$response = $pendingstep->get_qt_data();
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_fraction($fraction);
$pendingstep->set_state($state);
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
}
return question_attempt::KEEP;
}
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_step()->get_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($fraction);
$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,43 @@
<?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 immediate feedback behaviour.
*
* @package qbehaviour_immediatefeedback
* @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 immediate feedback behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_immediatefeedback_type extends question_behaviour_type {
public function is_archetypal() {
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_immediatefeedback.
*
* @package qbehaviour_immediatefeedback
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_immediatefeedback\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_immediatefeedback 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,28 @@
<?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_immediatefeedback', language 'en'.
*
* @package qbehaviour
* @subpackage immediatefeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['notcomplete'] = 'Not complete';
$string['pluginname'] = 'Immediate feedback';
$string['privacy:metadata'] = 'The Immediate feedback question behaviour plugin does not store any personal data.';
@@ -0,0 +1,41 @@
<?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/>.
/**
* Defines the renderer for the immediate feedback behaviour.
*
* @package qbehaviour
* @subpackage immediatefeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the immediate
* feedback behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_immediatefeedback_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
return $this->submit_button($qa, $options);
}
}
@@ -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_immediatefeedback;
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 immediate feedback behaviour type class.
*
* @package qbehaviour_immediatefeedback
* @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_immediatefeedback_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('immediatefeedback');
}
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,240 @@
<?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_immediatefeedback;
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 immediate feedback behaviour.
*
* @package qbehaviour_immediatefeedback
* @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_immediatefeedback_feedback_multichoice_right(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatefeedback');
$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_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());
// 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_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());
// Submit the right answer.
$this->process_submission(array('answer' => $rightindex, '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(1);
$this->check_current_output(
$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->assertEquals('A',
$this->quba->get_response_summary($this->slot));
$numsteps = $this->get_step_count();
// Now try to save again - as if the user clicked next in the quiz.
$this->process_submission(array('answer' => $rightindex));
// Verify.
$this->assertEquals($numsteps, $this->get_step_count());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(1);
$this->check_current_output(
$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());
// Finish the attempt - should not need to add a new state.
$this->quba->finish_all_questions();
// Verify.
$this->assertEquals($numsteps, $this->get_step_count());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(1);
$this->check_current_output(
$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());
// 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_partcorrect_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
$mc->answers[15]->fraction = 1;
$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_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 2);
$this->assertEqualsWithDelta($autogradedstep->get_fraction(), -0.3333333, 0.0000001);
}
public function test_immediatefeedback_feedback_multichoice_try_to_submit_blank(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatefeedback');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
// Submit nothing.
$this->process_submission(array('-submit' => 1));
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->check_current_output(
$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_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_contains_validation_error_expectation());
$this->assertNull($this->quba->get_response_summary($this->slot));
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_mc_radio_expectation(0, false, false),
$this->get_contains_mc_radio_expectation(1, false, false),
$this->get_contains_mc_radio_expectation(2, false, false));
// 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_partcorrect_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
}
public function test_immediatefeedback_feedback_multichoice_wrong_on_finish(): void {
// Create a true-false question with correct answer true.
$mc = \test_question_maker::make_a_multichoice_single_question();
$this->start_attempt_at_question($mc, 'immediatefeedback');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$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());
$rightindex = $this->get_mc_right_answer_index($mc);
$wrongindex = ($rightindex + 1) % 3;
// 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_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());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(-0.3333333);
$this->check_current_output(
$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->assertMatchesRegularExpression('/B|C/',
$this->quba->get_response_summary($this->slot));
}
}
@@ -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 immediatefeedback
* @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_immediatefeedback';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,151 @@
<?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/>.
/**
* This behaviour is for information items.
*
* @package qbehaviour
* @subpackage informationitem
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Question behaviour informaiton items.
*
* For example for the 'Description' 'Question type'. There is no grade,
* and the question type is marked complete the first time the user navigates
* away from a page that contains that question.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_informationitem extends question_behaviour {
public function is_compatible_question(question_definition $question) {
return true;
}
public function get_expected_data() {
if ($this->qa->get_state() == question_state::$todo) {
return array('seen' => PARAM_BOOL);
}
return parent::get_expected_data();
}
public function get_correct_response() {
if ($this->qa->get_state() == question_state::$todo) {
return array('seen' => 1);
}
return array();
}
public function adjust_display_options(question_display_options $options) {
parent::adjust_display_options($options);
$options->marks = question_display_options::HIDDEN;
// At the moment, the code exists to process a manual comment on an
// information item, but we don't display the UI unless there is already
// a comment.
if (!$this->qa->get_state()->is_commented()) {
$options->manualcomment = question_display_options::HIDDEN;
}
}
public function get_state_string($showcorrectness) {
return '';
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
} else if ($pendingstep->has_behaviour_var('seen')) {
return $this->process_seen($pendingstep);
} else {
return question_attempt::DISCARD;
}
}
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('seen')) {
return get_string('seen', 'qbehaviour_informationitem');
}
return $this->summarise_start($step);
}
public function process_comment(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('mark')) {
throw new coding_exception('Information items cannot be graded.');
}
return parent::process_comment($pendingstep);
}
/**
* Handle the 'finish' case of {@see process_action()}.
*
* @param question_attempt_pending_step $pendingstep step representing the action.
* @return bool either {@see question_attempt::KEEP} or {@see question_attempt::DISCARD}.
*/
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
if (!$this->qa->get_state()->is_active()) {
throw new coding_exception('It should be impossible for question_attempt ' . $this->qa->get_database_id() .
' using ' . get_class($this) . " to receive a 'finish' action while not active. " .
'Failing with an error, rather than continuing and doing undefined processing, ' .
'so the bug can be identified.');
}
$pendingstep->set_state(question_state::$finished);
return question_attempt::KEEP;
}
/**
* Handle the 'seen' case of {@see process_action()}.
*
* @param question_attempt_pending_step $pendingstep step representing the action.
* @return bool either {@see question_attempt::KEEP} or {@see question_attempt::DISCARD}.
*/
public function process_seen(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
// Assert so we get a clear error message if the assumptions on which this code relies is invalid.
if (!$this->qa->get_state()->is_active()) {
throw new coding_exception('It should be impossible for question_attempt ' . $this->qa->get_database_id() .
' using ' . get_class($this) . " to receive a 'seen' action while not active. " .
'Failing with an error, rather than continuing and doing undefined processing, ' .
'so the bug can be identified.');
}
$pendingstep->set_state(question_state::$complete);
return question_attempt::KEEP;
}
}
@@ -0,0 +1,36 @@
<?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 information item behaviour.
*
* @package qbehaviour_informationitem
* @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 informationitem items.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_informationitem_type extends question_behaviour_type {
}
@@ -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_informationitem.
*
* @package qbehaviour_informationitem
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_informationitem\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_informationitem 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,28 @@
<?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_informationitem', language 'en'.
*
* @package qbehaviour
* @subpackage informationitem
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Behaviour for information items';
$string['privacy:metadata'] = 'The Information items question behaviour plugin does not store any personal data.';
$string['seen'] = 'Seen';
@@ -0,0 +1,48 @@
<?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/>.
/**
* Defines the renderer the information item behaviour.
*
* @package qbehaviour_informationitem
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the information
* item behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_informationitem_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
if ($options->readonly || $qa->get_state() != question_state::$todo) {
return '';
}
// Hidden input to move the question into the complete state.
return html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_behaviour_field_name('seen'),
'value' => 1,
));
}
}
@@ -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_informationitem;
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 information item behaviour type class.
*
* @package qbehaviour_informationitem
* @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_informationitem_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('informationitem');
}
public function test_is_archetypal(): void {
$this->assertFalse($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->assertFalse($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,137 @@
<?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_informationitem;
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 information item behaviour.
*
* @package qbehaviour_informationitem
* @category test
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbehaviour_informationitem
*/
class walkthrough_test extends \qbehaviour_walkthrough_test_base {
public function test_informationitem_feedback_description(): void {
// Create a true-false question with correct answer true.
$description = \test_question_maker::make_question('description');
$this->start_attempt_at_question($description, 'deferredfeedback');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($description),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . '-seen', 1),
$this->get_does_not_contain_feedback_expectation());
// Check no hidden input when read-only.
$this->displayoptions->readonly = true;
$this->check_current_output($this->get_contains_question_text_expectation($description),
$this->get_does_not_contain_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . '-seen', 1));
$this->displayoptions->readonly = false;
// Process a submission indicating this question has been seen.
$this->process_submission(['-seen' => 1]);
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output($this->get_does_not_contain_correctness_expectation(),
new \question_no_pattern_expectation(
'/type=\"hidden\"[^>]*name=\"[^"]*seen\"|name=\"[^"]*seen\"[^>]*type=\"hidden\"/'),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$finished);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_question_text_expectation($description),
$this->get_contains_general_feedback_expectation($description));
// Process a manual comment.
$this->manual_grade('Not good enough!', null, FORMAT_HTML);
$this->check_current_state(question_state::$manfinished);
$this->check_current_mark(null);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Check that trying to process a manual comment with a grade causes an exception.
$this->expectException('moodle_exception');
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
}
public function test_informationitem_regrade(): void {
// Create a true-false question with correct answer true.
$description = \test_question_maker::make_question('description');
$this->start_attempt_at_question($description, 'deferredfeedback');
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($description),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . '-seen', 1),
$this->get_does_not_contain_feedback_expectation());
// Process a submission indicating this question has been seen.
$this->process_submission(['-seen' => 1]);
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output($this->get_does_not_contain_correctness_expectation(),
new \question_no_pattern_expectation(
'/type=\"hidden\"[^>]*name=\"[^"]*seen\"|name=\"[^"]*seen\"[^>]*type=\"hidden\"/'),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$finished);
$this->check_current_mark(null);
$this->check_step_count(3);
$this->check_current_output(
$this->get_contains_question_text_expectation($description),
$this->get_contains_general_feedback_expectation($description));
// Regrade the attempt.
$this->quba->regrade_all_questions(true);
// Verify.
$this->check_current_mark(null);
$this->check_step_count(3);
$this->check_current_output(
$this->get_contains_question_text_expectation($description),
$this->get_contains_general_feedback_expectation($description));
}
}
@@ -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 informationitem
* @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_informationitem';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -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;
@@ -0,0 +1,93 @@
<?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 that is like the interactive behaviour, but where the
* student is credited for parts of the question they got right on earlier tries.
*
* @package qbehaviour
* @subpackage interactivecountback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../interactive/behaviour.php');
/**
* Question behaviour for interactive mode with count-back scoring.
*
* As an example, suppose we have a matching question with 4 parts, and 3 tries
* (penalty 1/3), and the question is worth 12 marks (so, 3 marks for each part).
* Suppose also that:
* - on the first try, the student gets the first two parts right, and the
* other two wrong.
* - on the second try, they are sure they got the first part right, so keep
* their answer the same, but they change their answer to the second part.
* They also get the answer to the thrid part right on this try, but still
* get the 4th part wrong.
* - On the final try, they get the first 3 parts right, but the 4th part still
* wrong.
* We want to grade them as follows.
* - For the first part, they were right first time, and did not change their
* answer, so we credit that part as right first time: 3/3
* - For the second part, although they were right first time, they then changed
* their mind, an only finally got it right on the third try, so 1/3.
* - For the third part, they got it right on the second try, and then did not
* change their answer, so 2/3.
* - For the last part, they were wrong at the last try, so 0/3.
* So, total mark is 6/12. (Really, a fraction of 0.5.)
*
* Of course, the details of the grading are actually up to the particular
* question type. The point is that the final grade can take into account all
* of the tries the student made.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactivecountback extends qbehaviour_interactive {
public function is_compatible_question(question_definition $question) {
return $question instanceof question_automatically_gradable_with_countback;
}
protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
$totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
$responses = array();
$lastsave = array();
foreach ($this->qa->get_step_iterator() as $step) {
if ($step->has_behaviour_var('submit') &&
$step->get_state() != question_state::$invalid) {
$responses[] = $step->get_qt_data();
$lastsave = array();
} else {
$lastsave = $step->get_qt_data();
}
}
$lastresponse = $pendingstep->get_qt_data();
if (!empty($lastresponse)) {
$responses[] = $lastresponse;
} else if (!empty($lastsave)) {
$responses[] = $lastsave;
}
return $this->question->compute_final_grade($responses, $totaltries);
}
}
@@ -0,0 +1,41 @@
<?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 with count-back scoring behaviour.
*
* @package qbehaviour_interactivecountback
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../interactive/behaviourtype.php');
/**
* Question behaviour type information for interactive behaviour with count-back scoring.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactivecountback_type extends qbehaviour_interactive_type {
public function is_archetypal() {
return false;
}
}
@@ -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_interactivecountback.
*
* @package qbehaviour_interactivecountback
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_interactivecountback\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_interactivecountback 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,27 @@
<?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_interactivecountback', language 'en'.
*
* @package qbehaviour
* @subpackage interactivecountback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Interactive with multiple tries (credit for earlier tries)';
$string['privacy:metadata'] = 'The Interactive with multiple tries (credit for earlier tries) question behaviour plugin does not store any personal data.';
@@ -0,0 +1,43 @@
<?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/>.
/**
* Defines the renderer for the interactive with countback behaviour.
*
* @package qbehaviour
* @subpackage interactivecountback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../interactive/renderer.php');
/**
* Renderer for outputting parts of a question belonging to the interactive with
* countback behaviour.
*
* There are not differences from the interactive output. We just need a class
* definition.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactivecountback_renderer extends qbehaviour_interactive_renderer {
}
@@ -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_interactivecountback;
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 and countback scoring behaviour type class.
*
* @package qbehaviour_interactivecountback
* @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_interactivecountback_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('interactivecountback');
}
public function test_is_archetypal(): void {
$this->assertFalse($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,147 @@
<?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_interactivecountback;
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 with countback behaviour.
*
* @package qbehaviour_interactivecountback
* @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_match_reset(): void {
// Create a matching question.
$m = \test_question_maker::make_question('match');
$m->shufflestems = false;
$m->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($m, 'interactive', 12);
$choiceorder = $m->get_choice_order();
$orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
$choices = [];
foreach ($choiceorder as $key => $choice) {
$choices[$key] = $m->choices[$choice];
}
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->assertEquals('interactivecountback',
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
$this->check_current_output(
$this->get_contains_question_text_expectation($m),
$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_num_parts_correct(),
$this->get_no_hint_visible_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub0', $choices, null, true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, null, true));
// Submit an answer with two right, and two wrong.
$this->process_submission(array('sub0' => $orderforchoice[1],
'sub1' => $orderforchoice[1], 'sub2' => $orderforchoice[1],
'sub3' => $orderforchoice[1], '-submit' => 1));
// Verify.
$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_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(2),
$this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub0', $orderforchoice[1]),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub1', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub2', '0'),
$this->get_contains_hidden_expectation(
$this->quba->get_field_prefix($this->slot) . 'sub3', $orderforchoice[1]));
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[1], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[1], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false));
// Check that extract responses will return the reset data.
$prefix = $this->quba->get_field_prefix($this->slot);
$this->assertEquals(array('sub0' => 1),
$this->quba->extract_responses($this->slot, array($prefix . 'sub0' => 1)));
// Do try again.
$this->process_submission(array('sub0' => $orderforchoice[1],
'sub3' => $orderforchoice[1], '-tryagain' => 1));
// Verify.
$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_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(2),
$this->get_no_hint_visible_expectation());
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], true),
$this->get_contains_select_expectation('sub1', $choices, null, true),
$this->get_contains_select_expectation('sub2', $choices, null, true),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], true));
// Submit the right answer.
$this->process_submission(array('sub0' => $orderforchoice[1],
'sub1' => $orderforchoice[2], 'sub2' => $orderforchoice[2],
'sub3' => $orderforchoice[1], '-submit' => 1));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(10);
$this->check_current_output(
$this->get_does_not_contain_submit_button_expectation(),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_contains_standard_correct_combined_feedback_expectation(),
new \question_no_pattern_expectation('/class="control\b[^"]*\bpartiallycorrect"/'));
$this->check_output_contains_selectoptions(
$this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
$this->get_contains_select_expectation('sub1', $choices, $orderforchoice[2], false),
$this->get_contains_select_expectation('sub2', $choices, $orderforchoice[2], false),
$this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false));
}
}
@@ -0,0 +1,36 @@
<?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 interactivecountback
* @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_interactivecountback';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->dependencies = [
'qbehaviour_interactive' => 2024041600,
];
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,120 @@
<?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 for questions that can only be graded manually.
*
* @package qbehaviour
* @subpackage manualgraded
* @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 questions that can only be graded manually.
*
* The student enters their response during the attempt, and it is saved. Later,
* when the whole attempt is finished, the attempt goes into the NEEDS_GRADING
* state, and the teacher must grade it manually.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_manualgraded extends question_behaviour_with_save {
public function is_compatible_question(question_definition $question) {
return $question instanceof question_with_responses;
}
public function adjust_display_options(question_display_options $options) {
parent::adjust_display_options($options);
if ($this->qa->get_state()->is_finished()) {
// Hide all feedback except genfeedback and manualcomment.
$save = clone($options);
$options->hide_all_feedback();
$options->generalfeedback = $save->generalfeedback;
$options->manualcomment = $save->manualcomment;
}
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
} else {
return $this->process_save($pendingstep);
}
}
/**
* Like the parent method, except that when a response is gradable, but not
* completely, we move it to the invalid state.
* @param question_attempt_pending_step $pendingstep a partially initialised step
* containing all the information about the action that is being performed.
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
*/
public function process_save(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
} else if (!$this->qa->get_state()->is_active()) {
throw new coding_exception('Question is not active, cannot process_actions.');
}
if ($this->is_same_response($pendingstep)) {
return question_attempt::DISCARD;
}
if ($this->is_complete_response($pendingstep)) {
$pendingstep->set_state(question_state::$complete);
} else if ($this->question->is_gradable_response($pendingstep->get_qt_data())) {
$pendingstep->set_state(question_state::$invalid);
} else {
$pendingstep->set_state(question_state::$todo);
}
return question_attempt::KEEP;
}
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 {
return $this->summarise_save($step);
}
}
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_step()->get_qt_data();
if (!$this->question->is_gradable_response($response)) {
$pendingstep->set_state(question_state::$gaveup);
} else {
$pendingstep->set_state(question_state::$needsgrading);
}
$pendingstep->set_new_response_summary($this->question->summarise_response($response));
return question_attempt::KEEP;
}
}
@@ -0,0 +1,44 @@
<?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 manually graded behaviour.
*
* @package qbehaviour_manualgraded
* @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 manually graded behaviour.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_manualgraded_type extends question_behaviour_type {
public function is_archetypal() {
return true;
}
public function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');
}
}
@@ -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_manualgraded.
*
* @package qbehaviour_manualgraded
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_manualgraded\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_manualgraded 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,49 @@
<?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/>.
/**
* Post-install script for manual graded question behaviour.
* @package qbehaviour_manualgraded
* @copyright 2013 The Open Universtiy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Post-install script
*/
function xmldb_qbehaviour_manualgraded_install() {
// Hide the manualgraded behaviour from the list of behaviours that users
// can select in the user-interface. If a user accidentally chooses manual
// graded behaviour for a quiz, there is no way to get the questions automatically
// graded after the student has answered them. If teachers really want to do
// this they can ask their admin to enable it on the manage behaviours
// screen in the UI.
$disabledbehaviours = get_config('question', 'disabledbehaviours');
if (!empty($disabledbehaviours)) {
$disabledbehaviours = explode(',', $disabledbehaviours);
} else {
$disabledbehaviours = array();
}
if (array_search('manualgraded', $disabledbehaviours) === false) {
$disabledbehaviours[] = 'manualgraded';
set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
}
}
@@ -0,0 +1,42 @@
<?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/>.
/**
* Post-install script for the manual graded question behaviour.
*
* @package qbehaviour_manualgraded
* @copyright 2013 The Open Universtiy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Manual graded question behaviour upgrade code.
*/
function xmldb_qbehaviour_manualgraded_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,27 @@
<?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_manualgraded', language 'en'.
*
* @package qbehaviour
* @subpackage manualgraded
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Manually graded';
$string['privacy:metadata'] = 'The Manually graded question behaviour plugin does not store any personal data.';
@@ -0,0 +1,38 @@
<?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/>.
/**
* Defines the renderer for the manual graded behaviour.
*
* @package qbehaviour
* @subpackage manualgraded
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question belonging to the manual
* graded behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_manualgraded_renderer extends qbehaviour_renderer {
}
@@ -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_manualgraded;
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 manually graded behaviour type class.
*
* @package qbehaviour_manualgraded
* @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_manualgraded_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('manualgraded');
}
public function test_is_archetypal(): void {
$this->assertTrue($this->behaviourtype->is_archetypal());
}
public function test_get_unused_display_options(): void {
$this->assertEquals(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
$this->behaviourtype->get_unused_display_options());
}
public function test_can_questions_finish_during_the_attempt(): void {
$this->assertFalse($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,717 @@
<?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_manualgraded;
use question_display_options;
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 manual graded behaviour.
*
* @package qbehaviour_manualgraded
* @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_manual_graded_essay(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Process the same data again, check it does not create a new step.
$numsteps = $this->get_step_count();
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
$this->process_submission(array('answer' => '', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$todo);
// Change back, check it creates a new step.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 2);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a manual comment.
$this->manual_grade('Not good enough!', 10, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrright);
$this->check_current_mark(10);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
// Now change the max mark for the question and regrade.
$this->quba->regrade_question($this->slot, true, 1);
// Verify.
$this->check_current_state(question_state::$mangrright);
$this->check_current_mark(1);
}
public function test_manual_graded_essay_not_answered(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->assertEquals('',
$this->quba->get_response_summary($this->slot));
// Process a manual comment.
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Not good enough!') . '/'));
// Now change the max mark for the question and regrade.
$this->quba->regrade_question($this->slot, true, 1);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.1);
}
public function test_manual_graded_truefalse(): void {
// Create a true-false question with correct answer true.
$tf = \test_question_maker::make_question('truefalse', 'true');
$this->start_attempt_at_question($tf, 'manualgraded', 2);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_question_text_expectation($tf),
$this->get_does_not_contain_feedback_expectation());
// Process a true answer and check the expected result.
$this->process_submission(array('answer' => 1));
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_contains_tf_true_radio_expectation(true, true),
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_specific_feedback_expectation());
// Process a manual comment.
$this->manual_grade('Not good enough!', 1, FORMAT_HTML);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_does_not_contain_correctness_expectation(),
$this->get_does_not_contain_specific_feedback_expectation(),
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
}
public function test_manual_grade_ungraded_question(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 0);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a manual comment. Note: null mark is the whole point here.
$this->manual_grade('Not good enough!', null, FORMAT_HTML);
// Verify.
// I am pretty sure this next assertion is incorrect. We should change
// the question state to indicate that this quetion has now been commented
// on. However, that is tricky, because what if, after that, the mam mark
// for the qusetions is changed. So, for now, this assertion verifies
// the current behaviour.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Not good enough!', '/') . '/'));
}
public function test_manual_graded_ignore_repeat_sumbission(): void {
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a blank manual comment. Ensure it does not change the state.
$numsteps = $this->get_step_count();
$this->manual_grade('', '', FORMAT_HTML);
$this->check_step_count($numsteps);
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
// Process a comment, but with the mark blank. Should be recorded, but
// not change the mark.
$this->manual_grade('I am not sure what grade to award.', '', FORMAT_HTML);
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
new \question_pattern_expectation('/' .
preg_quote('I am not sure what grade to award.', '/') . '/'));
// Now grade it.
$this->manual_grade('Pretty good!', '9.00000', FORMAT_HTML);
$this->check_step_count($numsteps + 2);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(9);
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Pretty good!', '/') . '/'));
// Process the same data again, and make sure it does not add a step.
$this->manual_grade('Pretty good!', '9.00000', FORMAT_HTML);
$this->check_step_count($numsteps + 2);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(9);
// Now set the mark back to blank.
$this->manual_grade('Actually, I am not sure any more.', '', FORMAT_HTML);
$this->check_step_count($numsteps + 3);
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
new \question_pattern_expectation('/' .
preg_quote('Actually, I am not sure any more.', '/') . '/'));
$qa = $this->quba->get_question_attempt($this->slot);
$this->assertEquals('Commented: Actually, I am not sure any more.',
$qa->summarise_action($qa->get_last_step()));
}
public function test_manual_graded_ignore_repeat_sumbission_commas(): void {
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Now grade it with a mark with a comma.
$numsteps = $this->get_step_count();
$this->manual_grade('Pretty good!', '9,00000', FORMAT_HTML);
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(9);
$qa = $this->get_question_attempt();
$this->assertEquals('Manually graded 9 with comment: Pretty good!',
$qa->summarise_action($qa->get_last_step()));
$this->check_current_output(
new \question_pattern_expectation('/' . preg_quote('Pretty good!', '/') . '/'));
// Process the same mark with a dot. Verify it does not add a new step.
$this->manual_grade('Pretty good!', '9.00000', FORMAT_HTML);
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(9);
}
public function test_manual_graded_essay_can_grade_0(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a blank comment and a grade of 0.
$this->manual_grade('', 0, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrwrong);
$this->check_current_mark(0);
}
public function test_manual_graded_change_comment_format(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Finish the attempt.
$this->quba->finish_all_questions();
// Process an example comment and a grade of 0.
$this->manual_grade('example', 0, FORMAT_HTML);
// Verify the format is FORMAT_HTML.
$this->check_comment('example', FORMAT_HTML);
// Process the same grade and comment with different format.
$this->manual_grade('example', 0, FORMAT_MARKDOWN);
// Verify the format is FORMAT_MARKDOWN.
$this->check_comment('example', FORMAT_MARKDOWN);
}
public function test_manual_graded_respects_display_options(): void {
// This test is for MDL-43874. Manual comments were not respecting the
// Display options for feedback.
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a comment and a grade.
$this->manual_grade('This should only appear if the displya options allow it', 5, FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(5);
$this->displayoptions->manualcomment = question_display_options::HIDDEN;
$this->check_output_does_not_contain('This should only appear if the displya options allow it');
$this->displayoptions->manualcomment = question_display_options::VISIBLE;
$this->check_output_contains('This should only appear if the displya options allow it');
}
public function test_manual_graded_invalid_value_throws_exception(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Try to process a an invalid grade.
$this->expectException('coding_exception');
$this->manual_grade('Comment', 'frog', FORMAT_HTML);
}
public function test_manual_graded_out_of_range_throws_exception(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output($this->get_contains_question_text_expectation($essay),
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new \question_contains_tag_with_attribute('textarea', 'name',
$this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
$this->get_does_not_contain_feedback_expectation());
// Finish the attempt.
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Try to process a an invalid grade.
$this->expectException('coding_exception');
$this->manual_grade('Comment', '10.1', FORMAT_HTML);
}
public function test_manual_graded_displays_proper_comment_format(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
// Check the right model is being used.
$this->assertEquals('manualgraded', $this->quba->get_question_attempt(
$this->slot)->get_behaviour_name());
// Simulate some data submitted by the student.
$this->process_submission(
array(
'answer' => "A submission!",
'answerformat' => FORMAT_PLAIN
)
);
// Finish the attempt.
$this->quba->finish_all_questions();
// Write a manual comment in markdown.
$this->manual_grade("*one\n*two\n*three\n", 10, FORMAT_MARKDOWN);
// Check that feedback contains the original markdown format.
$preg = '/<textarea [^>]+name="[^"]+-comment"[^>]+>\*one\n\*two\n\*three\n/';
$this->displayoptions->manualcomment = question_display_options::EDITABLE;
$this->check_current_output(
new \question_pattern_expectation($preg)
);
}
public function test_manual_grading_reshows_exactly_the_mark_input(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question graded out of 15 and attempt it.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 15);
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->assertEquals('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Try to process a grade where the score will be stored rounded.
$this->manual_grade('Comment', '5.0', FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(5);
$this->displayoptions->manualcomment = question_display_options::EDITABLE;
$this->render();
$this->check_output_contains_text_input('-mark', '5.0');
// Rescale what the question is worth, and verify the display.
$this->get_question_attempt()->set_max_mark(1);
$this->render();
$this->check_output_contains_text_input('-mark', '0.3333333');
}
public function test_manual_grading_history_display(): void {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question graded out of 15 and attempt it.
$essay = \test_question_maker::make_an_essay_question();
$this->start_attempt_at_question($essay, 'deferredfeedback', 10);
$this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->quba->finish_all_questions();
// Verify.
$this->check_current_state(question_state::$needsgrading);
// Process an initial grade and comment.
$this->manual_grade('First comment', '5.0', FORMAT_HTML);
// Process a second grade and comment.
$this->manual_grade('Second comment', '7.0', FORMAT_HTML);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(7);
$this->displayoptions->history = question_display_options::VISIBLE;
$this->render();
$this->check_output_contains('Manually graded 5 with comment: First comment');
$this->check_output_contains('Manually graded 7 with comment: Second comment');
}
}
@@ -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 manualgraded
* @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_manualgraded';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+76
View File
@@ -0,0 +1,76 @@
<?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/>.
/**
* Fake question behaviour that is used when the actual behaviour was not
* available.
*
* @package qbehaviour
* @subpackage missing
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Fake question behaviour that is used when the actual behaviour
* is not available.
*
* Imagine, for example, that a quiz attempt has been restored from another
* Moodle site with more behaviours installed, or a behaviour
* that used to be available in this site has been uninstalled. Obviously all we
* can do is have some code to prevent fatal errors.
*
* The approach we take is: The rendering code is still implemented, as far as
* possible. A warning is shown that behaviour specific bits may be missing.
* Any attempt to process anything causes an exception to be thrown.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_missing extends question_behaviour {
public function is_compatible_question(question_definition $question) {
return true;
}
public function summarise_action(question_attempt_step $step) {
return '';
}
public function init_first_step(question_attempt_step $step, $variant) {
throw new coding_exception('The behaviour used for this question is not available. ' .
'No processing is possible.');
}
public function process_action(question_attempt_pending_step $pendingstep) {
throw new coding_exception('The behaviour used for this question is not available. ' .
'No processing is possible.');
}
public function get_min_fraction() {
throw new coding_exception('The behaviour used for this question is not available. ' .
'No processing is possible.');
}
public function get_max_fraction() {
throw new coding_exception('The behaviour used for this question is not available. ' .
'No processing is possible.');
}
}
@@ -0,0 +1,38 @@
<?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/>.
/**
* Fake question behaviour type that is used when the actual behaviour is not
* available.
*
* @package qbehaviour_missing
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Fake question behaviour type information that is used when the actual
* behaviour is not available.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_missing_type extends question_behaviour_type {
}
@@ -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_missing.
*
* @package qbehaviour_missing
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_missing\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qbehaviour_missing 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,28 @@
<?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_missing', language 'en'.
*
* @package qbehaviour
* @subpackage missing
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Missing behaviour';
$string['questionusedunknownmodel'] = 'This question was attempted with a behaviour that is not currently available. The question is being displayed as well as possible, but some parts may be missing or wrong.';
$string['privacy:metadata'] = 'The Missing question behaviour plugin does not store any personal data.';
+43
View File
@@ -0,0 +1,43 @@
<?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/>.
/**
* Defines the renderer for when the actual behaviour used is not available.
*
* @package qbehaviour
* @subpackage missing
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer for outputting parts of a question when the actual behaviour
* used is not available.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_missing_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
return html_writer::tag('div',
get_string('questionusedunknownmodel', 'qbehaviour_missing'),
array('class' => 'warning'));
}
}
@@ -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_missing;
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 missing behaviour type stand-in class.
*
* @package qbehaviour_missing
* @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_missing_type */
protected $behaviourtype;
public function setUp(): void {
parent::setUp();
$this->behaviourtype = question_engine::get_behaviour_type('missing');
}
public function test_is_archetypal(): void {
$this->assertFalse($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->assertFalse($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,119 @@
<?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_missing;
use qbehaviour_missing;
use question_attempt;
use question_attempt_pending_step;
use question_attempt_step;
use question_bank;
use question_display_options;
use question_state;
use question_test_recordset;
use question_usage_null_observer;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/../../../engine/lib.php');
require_once(__DIR__ . '/../../../engine/tests/helpers.php');
require_once(__DIR__ . '/../behaviour.php');
/**
* Unit tests for the 'missing' behaviour.
*
* @package qbehaviour_missing
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class missingbehaviour_test extends \advanced_testcase {
public function test_missing_cannot_start(): void {
$qa = new question_attempt(\test_question_maker::make_question('truefalse', 'true'), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException(\moodle_exception::class);
$behaviour->init_first_step(new question_attempt_step(array()), 1);
}
public function test_missing_cannot_process(): void {
$qa = new question_attempt(\test_question_maker::make_question('truefalse', 'true'), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException(\moodle_exception::class);
$behaviour->process_action(new question_attempt_pending_step(array()));
}
public function test_missing_cannot_get_min_fraction(): void {
$qa = new question_attempt(\test_question_maker::make_question('truefalse', 'true'), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException(\moodle_exception::class);
$behaviour->get_min_fraction();
}
public function test_missing_cannot_get_max_fraction(): void {
$qa = new question_attempt(\test_question_maker::make_question('truefalse', 'true'), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException(\moodle_exception::class);
$behaviour->get_max_fraction();
}
public function test_render_missing(): void {
$records = new question_test_recordset(array(
array('questionattemptid', 'contextid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary',
'timemodified', 'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '',
1256233790, 1, 0, 'todo', null, 1256233700, 1, '_order', '1,2,3'),
array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '',
1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, '-submit', '1'),
array(1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '',
1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, 'choice0', '1'),
));
$question = \test_question_maker::make_question('truefalse', 'true');
$question->id = -1;
question_bank::start_unit_test();
question_bank::load_test_question_data($question);
$qa = question_attempt::load_from_records($records, 1,
new question_usage_null_observer(), 'deferredfeedback');
question_bank::end_unit_test();
$this->assertEquals(2, $qa->get_num_steps());
$step = $qa->get_step(0);
$this->assertEquals(question_state::$todo, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEquals(1256233700, $step->get_timecreated());
$this->assertEquals(1, $step->get_user_id());
$this->assertEquals(array('_order' => '1,2,3'), $step->get_all_data());
$step = $qa->get_step(1);
$this->assertEquals(question_state::$complete, $step->get_state());
$this->assertEquals(0.5, $step->get_fraction());
$this->assertEquals(1256233705, $step->get_timecreated());
$this->assertEquals(1, $step->get_user_id());
$this->assertEquals(array('-submit' => '1', 'choice0' => '1'), $step->get_all_data());
$output = $qa->render(new question_display_options(), '1');
$this->assertMatchesRegularExpression('/' . preg_quote($qa->get_question(false)->questiontext, '/') . '/', $output);
$this->assertMatchesRegularExpression('/' . preg_quote(
get_string('questionusedunknownmodel', 'qbehaviour_missing'), '/') . '/', $output);
$this->assertTag(array('tag'=>'div', 'attributes'=>array('class'=>'warning')), $output);
}
}
+33
View File
@@ -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 missing
* @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_missing';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+297
View File
@@ -0,0 +1,297 @@
<?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/>.
/**
* Defines the renderer base class for question behaviours.
*
* @package moodlecore
* @subpackage questionbehaviours
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Renderer base class for question behaviours.
*
* The methods in this class are mostly called from {@link core_question_renderer}
* which coordinates the overall output of questions.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qbehaviour_renderer extends plugin_renderer_base {
/**
* Generate some HTML (which may be blank) that appears in the question
* formulation area, afer the question type generated output.
*
* For example.
* immediatefeedback and interactive mode use this to show the Submit button,
* and CBM use this to display the certainty choices.
*
* @param question_attempt $qa a question attempt.
* @param question_display_options $options controls what should and should not be displayed.
* @return string HTML fragment.
*/
public function controls(question_attempt $qa, question_display_options $options) {
return '';
}
/**
* Generate some HTML (which may be blank) that appears in the outcome area,
* after the question-type generated output.
*
* For example, the CBM models use this to display an explanation of the score
* adjustment that was made based on the certainty selected.
*
* @param question_attempt $qa a question attempt.
* @param question_display_options $options controls what should and should not be displayed.
* @return string HTML fragment.
*/
public function feedback(question_attempt $qa, question_display_options $options) {
return '';
}
public function manual_comment_fields(question_attempt $qa, question_display_options $options) {
global $CFG;
require_once($CFG->dirroot.'/lib/filelib.php');
require_once($CFG->dirroot.'/repository/lib.php');
$inputname = $qa->get_behaviour_field_name('comment');
$id = $inputname . '_id';
list($commenttext, $commentformat, $commentstep) = $qa->get_current_manual_comment();
$editor = editors_get_preferred_editor($commentformat);
$strformats = format_text_menu();
$formats = $editor->get_supported_formats();
foreach ($formats as $fid) {
$formats[$fid] = $strformats[$fid];
}
$draftitemareainputname = $qa->get_behaviour_field_name('comment:itemid');
$draftitemid = optional_param($draftitemareainputname, false, PARAM_INT);
if (!$draftitemid && $commentstep === null) {
$commenttext = '';
$draftitemid = file_get_unused_draft_itemid();
} else if (!$draftitemid) {
list($draftitemid, $commenttext) = $commentstep->prepare_response_files_draft_itemid_with_text(
'bf_comment', $options->context->id, $commenttext);
}
$editor->set_text($commenttext);
$editor->use_editor($id, question_utils::get_editor_options($options->context),
question_utils::get_filepicker_options($options->context, $draftitemid));
$commenteditor = html_writer::tag('div', html_writer::tag('textarea', s($commenttext),
array('id' => $id, 'name' => $inputname, 'rows' => 3, 'cols' => 60)));
$attributes = ['type' => 'hidden', 'name' => $draftitemareainputname, 'value' => $draftitemid];
$commenteditor .= html_writer::empty_tag('input', $attributes);
$editorformat = '';
if (count($formats) == 1) {
reset($formats);
$editorformat .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => $inputname . 'format', 'value' => key($formats)));
} else {
$editorformat = html_writer::start_tag('div', array('class' => 'fitem'));
$editorformat .= html_writer::start_tag('div', array('class' => 'fitemtitle'));
$editorformat .= html_writer::tag('label', get_string('format'), array('for'=>'menu'.$inputname.'format'));
$editorformat .= html_writer::end_tag('div');
$editorformat .= html_writer::start_tag('div', array('class' => 'felement fhtmleditor'));
$editorformat .= html_writer::select($formats, $inputname.'format', $commentformat, '');
$editorformat .= html_writer::end_tag('div');
$editorformat .= html_writer::end_tag('div');
}
$comment = html_writer::tag('div', html_writer::tag('div',
html_writer::tag('label', get_string('comment', 'question'),
array('for' => $id)), array('class' => 'fitemtitle')) .
html_writer::tag('div', $commenteditor, array('class' => 'felement fhtmleditor', 'data-fieldtype' => "editor")),
array('class' => 'fitem'));
$comment .= $editorformat;
$mark = '';
if ($qa->get_max_mark()) {
$currentmark = $qa->get_current_manual_mark();
$maxmark = $qa->get_max_mark();
$fieldsize = strlen($qa->format_max_mark($options->markdp)) - 1;
$markfield = $qa->get_behaviour_field_name('mark');
$attributes = array(
'type' => 'text',
'size' => $fieldsize,
'name' => $markfield,
'id'=> $markfield
);
if (!is_null($currentmark)) {
$attributes['value'] = $currentmark;
}
$markrange = html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_behaviour_field_name('maxmark'),
'value' => $maxmark,
)) . html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_control_field_name('minfraction'),
'value' => $qa->get_min_fraction(),
)) . html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_control_field_name('maxfraction'),
'value' => $qa->get_max_fraction(),
));
$error = $qa->validate_manual_mark($currentmark);
$errorclass = '';
if ($error !== '') {
$erroclass = ' error';
$error = html_writer::tag('span', $error,
array('class' => 'error')) . html_writer::empty_tag('br');
}
$a = new stdClass();
$a->max = $qa->format_max_mark($options->markdp);
$a->mark = html_writer::empty_tag('input', $attributes);
$mark = html_writer::tag('div', html_writer::tag('div',
html_writer::tag('label', get_string('mark', 'question'),
array('for' => $markfield)),
array('class' => 'fitemtitle')) .
html_writer::tag('div', $error . get_string('xoutofmax', 'question', $a) .
$markrange, array('class' => 'felement ftext' . $errorclass)
), array('class' => 'fitem'));
}
return html_writer::tag('fieldset', html_writer::tag('div', $comment . $mark,
array('class' => 'fcontainer clearfix')), array('class' => 'hidden'));
}
public function manual_comment_view(question_attempt $qa, question_display_options $options) {
$output = '';
if ($qa->has_manual_comment()) {
$output .= get_string('commentx', 'question',
$qa->get_behaviour(false)->format_comment(null, null, $options->context));
}
if ($options->manualcommentlink) {
$url = new moodle_url($options->manualcommentlink, array('slot' => $qa->get_slot()));
$link = $this->output->action_link($url, get_string('commentormark', 'question'),
new popup_action('click', $url, 'commentquestion',
array('width' => 600, 'height' => 800)));
$output .= html_writer::tag('div', $link, array('class' => 'commentlink'));
}
return $output;
}
/**
* Display the manual comment, and a link to edit it, if appropriate.
*
* @param question_attempt $qa a question attempt.
* @param question_display_options $options controls what should and should not be displayed.
* @return string HTML fragment.
*/
public function manual_comment(question_attempt $qa, question_display_options $options) {
if ($options->manualcomment == question_display_options::EDITABLE) {
return $this->manual_comment_fields($qa, $options);
} else if ($options->manualcomment == question_display_options::VISIBLE) {
return $this->manual_comment_view($qa, $options);
} else {
return '';
}
}
/**
* Several behaviours need a submit button, so put the common code here.
* The button is disabled if the question is displayed read-only.
* @param question_display_options $options controls what should and should not be displayed.
* @return string HTML fragment.
*/
protected function submit_button(question_attempt $qa, question_display_options $options) {
if (!$qa->get_state()->is_active()) {
return '';
}
$attributes = array(
'type' => 'submit',
'id' => $qa->get_behaviour_field_name('submit'),
'name' => $qa->get_behaviour_field_name('submit'),
'value' => 1,
'class' => 'submit btn btn-secondary',
'data-savescrollposition' => 'true',
);
if ($options->readonly) {
$attributes['disabled'] = 'disabled';
}
$output = html_writer::tag('button',
$options->add_question_identifier_to_label(get_string('check', 'question'), true), $attributes);
if (!$options->readonly) {
$this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]);
}
return $output;
}
/**
* Return any HTML that needs to be included in the page's <head> when
* questions using this model are used.
* @param $qa the question attempt that will be displayed on the page.
* @return string HTML fragment.
*/
public function head_code(question_attempt $qa) {
return '';
}
/**
* Generate the display of the marks for this question.
* @param question_attempt $qa the question attempt to display.
* @param core_question_renderer $qoutput the renderer for standard parts of questions.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
public function mark_summary(question_attempt $qa, core_question_renderer $qoutput,
question_display_options $options) {
return $qoutput->standard_mark_summary($qa, $this, $options);
}
/**
* Generate the display of the available marks for this question.
* @param question_attempt $qa the question attempt to display.
* @param core_question_renderer $qoutput the renderer for standard parts of questions.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
public function marked_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
question_display_options $options) {
return $qoutput->standard_marked_out_of_max($qa, $options);
}
/**
* Generate the display of the marks for this question out of the available marks.
* @param question_attempt $qa the question attempt to display.
* @param core_question_renderer $qoutput the renderer for standard parts of questions.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
public function mark_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
question_display_options $options) {
return $qoutput->standard_mark_out_of_max($qa, $options);
}
}
+104
View File
@@ -0,0 +1,104 @@
This files describes API changes for question behaviour plugins.
=== 4.0 ===
1) The major question bank changes should not affect behaviour plugins.
The navigation changes may affect Behat tests. If you encounter this,
the best way to fix it is to use the new navigation steps in MDL-74130.
=== 3.10 ===
1) The slot parameter of method M.core_question_engine.init_submit_button now removed.
The method will get the unique id by using the 'Check' button element.
=== 3.1 ===
1) The standard behaviours that use a 'Check' button have all been changed so
that they only show the button when the question is active. Your behaviour
may interit this behaviour, because the change was made in the base class,
and this is probably good for consistency. However, if your question behaviour
uses the Check button, your probably want to test it carefully, and you will
probably have to update your unit tests. See MDL-53304 for more details.
=== 2.9 ===
1) There are new methods question_behaviour::can_finish_during_attempt and
question_behaviour_type::can_finish_during_attempt. These methods both return
false by default. You should override it if, with your behaviour, questions may
finish just through the student interacting with them (e.g. by clicking the
Check button within the question).
The behaviour type method answers the the question for this behaviour in
general, without reference to a specific question. The method on the
behaviour class answers the question for a specific attempt at a specific
question.
=== 2.7 ===
1) question_behaviour_type has a new method allows_multiple_submitted_responses
which defaults to false but should return true if this question behaviour
accepts multiple submissions of responses within one attempt eg. multiple
tries for the interactive or adaptive question behaviours.
question_behaviour has a new method step_has_a_submitted_response($step). For
question behaviours where it is not only the final response that is
submitted by the student, you need to override this method to return true
for other steps where a student has submitted a response. See
question_behaviour_with_multiple_tries::step_has_a_submitted_response($step)
for example. This method only needs to be overriden if you are returning
true from allows_multiple_response_submissions.
=== 2.6 ===
1) Legacy required_question_definition_type no longer supported. (See 2.2 point 2) below.)
2) Behaviours now have to define an extra class
class qbehaviour_mybehaviour_type extends question_behaviour_type {
This class returns information about the type of behaviour, as opposed to
the qbehaviour_mybehaviour class which controls a particular
question_attempt. That is like the difference between the qtype_mytype and
the qtype_mytype_question classes.
Practically, what this means is that any of the methods that used to be
static methods of qbehaviour_mybehaviour class are now normal instance
methods of the qbehaviour_mybehaviour_type class. Specifically.
2.5 / qbehaviour_mybehaviour -> 2.6 / qbehaviour_mybehaviour_type
IS_ARCHETYPAL -> is_archetypal()
adjust_random_guess_score() -> adjust_random_guess_score()
get_unused_display_options() -> get_unused_display_options()
3) The static method is_manual_grade_in_range has moved from the
question_behaviour class to the question_engine class.
4) Behaviours can now control how the marks information is displayed in the
grey info area to the left of the question. There is a new method
mark_summary that you can override, although the default implementation is
fine in most cases. it uses the marked_out_of_max and mark_out_of_max methods
as appropriate, so you may just wish to override those.
=== 2.3 ===
1) This plugin type now supports cron in the standard way. If required, Create a
lib.php file containing
function qbehaviour_mypluginname_cron() {};
=== 2.2 ===
1) The old
public static function get_required_behaviours()
method is no more. Instead use the ->dependencies facility in version.php. E.g.
$plugin->dependencies = array(
'qbehaviour_immediatefeedback' => 2011102700,
'qbehaviour_deferredcbm' => 2011102700
);
2) The old required_question_definition_type method has been replaced by a new
is_compatible_question method. You should change your behaviour to override
the new method, not the old one. This change has been implemented in a
backwards-compatible way, so behaviours will not break.