MDL-20636 Current work-in-progress converting the question engine to Moodle 2.0

This commit is contained in:
Tim Hunt 2010-12-20 16:16:09 +00:00
parent 5a7f931ee5
commit d1b7e03d5d
106 changed files with 19706 additions and 1725 deletions

0
mod/quiz/db/install.xml Executable file → Normal file
View File

0
mod/quiz/pix/icon.gif Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 101 B

After

Width:  |  Height:  |  Size: 101 B

View File

@ -0,0 +1,178 @@
<?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_adaptive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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_save {
const IS_ARCHETYPAL = true;
public function required_question_definition_type() {
return '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_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
public function adjust_display_options(question_display_options $options) {
parent::adjust_display_options($options);
if (!$this->qa->get_state()->is_finished() &&
$this->qa->get_last_behaviour_var('_try')) {
$options->feedback = true;
}
}
public function get_state_string($showcorrectness) {
$state = $this->qa->get_state();
$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($showcorrectness);
}
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_gradable_response($response)) {
$pendingstep->set_state(question_state::$invalid);
if ($this->qa->get_state() != question_state::$invalid) {
$status = question_attempt::KEEP;
}
return $status;
}
$prevtries = $this->qa->get_last_behaviour_var('_try', 0);
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
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;
}
$laststep = $this->qa->get_last_step();
$response = $laststep->get_qt_data();
if (!$this->question->is_gradable_response($response)) {
$pendingstep->set_state(question_state::$gaveup);
return question_attempt::KEEP;
}
$prevtries = $this->qa->get_last_behaviour_var('_try', 0);
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
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_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
$pendingstep->set_state($state);
$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;
}
}

View File

@ -0,0 +1,6 @@
<?php
$string['adaptive'] = 'Adaptive mode';
$string['gradingdetails'] = 'Marks for this submission: $a->raw/$a->max.';
$string['gradingdetailsadjustment'] = 'With previous penalties this gives <strong>$a->cur/$a->max</strong>.';
$string['gradingdetailspenalty'] = 'This submission attracted a penalty of $a.';

View File

@ -0,0 +1,98 @@
<?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_adaptive
* @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 {
protected function get_graded_step(question_attempt $qa) {
foreach ($qa->get_reverse_step_iterator() as $step) {
if ($step->has_behaviour_var('_try')) {
return $step;
}
}
}
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) {
// Try to find the last graded step.
$gradedstep = $this->get_graded_step($qa);
if (is_null($gradedstep) || $qa->get_max_mark() == 0 || !$options->marks) {
return '';
}
// Display the grading details from the last graded state
$mark = new stdClass;
$mark->max = $qa->format_max_mark($options->markdp);
$actualmark = $gradedstep->get_fraction() * $qa->get_max_mark();
$mark->cur = format_float($actualmark, $options->markdp);
$rawmark = $gradedstep->get_behaviour_var('_rawfraction') * $qa->get_max_mark();
$mark->raw = format_float($rawmark, $options->markdp);
// let student know wether the answer was correct
if ($qa->get_state()->is_commented()) {
$class = $qa->get_state()->get_feedback_class();
} else {
$class = question_state::graded_state_for_fraction(
$gradedstep->get_behaviour_var('_rawfraction'))->get_feedback_class();
}
$gradingdetails = get_string('gradingdetails', 'qbehaviour_adaptive', $mark);
$gradingdetails .= $this->penalty_info($qa, $mark);
$output = '';
$output .= html_writer::tag('div', get_string($class, 'question'),
array('class' => 'correctness ' . $class));
$output .= html_writer::tag('div', $gradingdetails,
array('class' => 'gradingdetails'));
return $output;
}
protected function penalty_info($qa, $mark) {
if (!$qa->get_question()->penalty) {
return '';
}
$output = '';
// print details of grade adjustment due to penalties
if ($mark->raw != $mark->cur) {
$output .= ' ' . get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark);
}
// print info about new penalty
// penalty is relevant only if the answer is not correct and further attempts are possible
if (!$qa->get_state()->is_finished()) {
$output .= ' ' . get_string('gradingdetailspenalty', 'qbehaviour_adaptive', $qa->get_question()->penalty);
}
return $output;
}
}

View File

@ -0,0 +1,239 @@
<?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 file contains tests that walks a question through the adaptive
* behaviour.
*
* @package qbehaviour_adaptive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_adaptive_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_adaptive_multichoice() {
// 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->assertPattern('/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->assertPattern('/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->assertEqual('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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
new PatternExpectation('/' . 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(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->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
}
public function test_adaptive_multichoice2() {
// 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());
// 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_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation());
}
public function test_adaptive_shortanswer_try_to_submit_blank() {
// Create a short answer question with correct answer true.
$sa = test_question_maker::make_a_shortanswer_question();
$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_contains_validation_error_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_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_contains_partcorrect_expectation(),
$this->get_contains_validation_error_expectation());
}
}

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/>.
/**
* Question behaviour for the old adaptive mode, with no penalties.
*
* @package qbehaviour_adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../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 {
const IS_ARCHETYPAL = true;
protected function adjusted_fraction($fraction, $prevtries) {
return $fraction;
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['adaptivenopenalty'] = 'Adaptive mode (no penalties)';

View File

@ -0,0 +1,34 @@
<?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_adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../adaptive/renderer.php');
class qbehaviour_adaptivenopenalty_renderer extends qbehaviour_adaptive_renderer {
protected function penalty_info($qa, $mark) {
return '';
}
}

View File

@ -0,0 +1,186 @@
<?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 file contains tests that walks a question through the adaptive (no penalties)k
* behaviour.
*
* @package qbehaviour_adaptivenopenalty
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_adaptivenopenalty_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_multichoice() {
// 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->assertPattern('/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->assertPattern('/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->assertEqual('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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
$this->get_contains_mark_summary(1),
new PatternExpectation('/' . 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(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->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
}
public function test_multichoice2() {
// 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());
// 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_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation());
}
}

View File

@ -0,0 +1,631 @@
<?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 quetsion 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
*/
/**
* 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 {
/**
* 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. These models should define the class constant
* IS_ARCHETYPAL as true. Other models are more implementation details, for
* example the informationitem model, or a special subclass like
* interactive_adapted_for_my_qtype. These models should IS_ARCHETYPAL as
* false.
* @var boolean
*/
const IS_ARCHETYPAL = false;
/** @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();
$requiredclass = $this->required_question_definition_type();
if (!$this->question instanceof $requiredclass) {
throw new Exception('This behaviour (' . $this->get_name() .
') cannot work with this question (' . get_class($this->question) . ')');
}
}
/**
* Most behaviours can only work with {@link question_definition}s
* of a particular subtype, or that implement a particular interface.
* This method lets the behaviour document that. The type of
* question passed to the constructor is then checked against this type.
* @return string class/interface name.
*/
public abstract function required_question_definition_type();
/**
* @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);
}
/**
* '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 static function get_unused_display_options() {
return array();
}
/**
* 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 unknown_type $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 HTML fragment.
*/
public function render(question_display_options $options, $number,
core_question_renderer $qoutput, qtype_renderer $qtoutput) {
$behaviouroutput = $this->get_renderer();
$options = clone($options);
$this->adjust_display_options($options);
return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
}
/**
* @return qbehaviour_renderer get the appropriate renderer to use for this model.
*/
public function get_renderer() {
return renderer_factory::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->init_first_step($step),
* but may be modified in some way by the model.
*
* @return number the minimum fraction when this question is attempted under
* this model.
*/
public function get_min_fraction() {
return 0;
}
/**
* 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 static function adjust_random_guess_score($fraction) {
return $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' => PARAM_RAW);
if ($this->qa->get_max_mark()) {
$vars['mark'] = question_attempt::PARAM_MARK;
$vars['maxmark'] = PARAM_NUMBER;
}
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();
}
/**
* @return array subpartid => object with fields
* ->responseclassid the
* ->response the actual response the student gave to this part, as a string.
* ->fraction the credit awarded for this subpart, may be null.
* returns an empty array if no analysis is possible.
*/
public function classify_response() {
return $this->question->classify_response($this->qa->get_last_qt_data());
}
/**
* Generate a brief textual description of the current state of the question,
* normally displayed under the question number.
*
* @param boolean $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.
*
* This method must call $this->question->init_first_step($step), and may
* perform additional processing if the model requries it.
*
* @param question_attempt_step $step the step being initialised.
*/
public function init_first_step(question_attempt_step $step) {
$this->question->init_first_step($step);
}
/**
* 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 boolean 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');
if (is_null($previouscomment) && !html_is_blank($newcomment) ||
$previouscomment != $newcomment) {
return false;
}
// So, now we know the comment is the same, so check the mark, if present.
$previousfraction = $this->qa->get_fraction();
$newmark = $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 boolean either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
*/
public abstract function process_action(question_attempt_pending_step $pendingstep);
/**
* 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 boolean 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')) {
$fraction = $pendingstep->get_behaviour_var('mark') / $pendingstep->get_behaviour_var('maxmark');
if ($pendingstep->get_behaviour_var('mark') === '') {
$fraction = null;
} else if ($fraction > 1 || $fraction < $this->qa->get_min_fraction()) {
throw new coding_exception('Score out of range when processing ' .
'a manual grading action.', $pendingstep);
}
$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.
* @return string the comment, ready to be output.
*/
public function format_comment($comment = null) {
$formatoptions = new stdClass;
$formatoptions->noclean = true;
$formatoptions->para = false;
if (is_null($comment)) {
$comment = $this->qa->get_manual_comment();
}
return format_text($comment, FORMAT_HTML, $formatoptions);
}
/**
* @return string a summary of a manual comment action.
* @param unknown_type $step
*/
protected function summarise_manual_comment($step) {
$a = new stdClass;
if ($step->has_behaviour_var('comment')) {
$a->comment = shorten_text(html_to_text($this->format_comment(
$step->get_behaviour_var('comment')), 0, false), 200);
} else {
$a->comment = '';
}
$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');
}
}
/**
* 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';
}
/**
* 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 boolean 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 boolean 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());
}
/**
* 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 boolean 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 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');
}
}
/**
* This helper class contains the constants and methods required for
* manipulating scores for certainly 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 $factor = array(
self::LOW => 0.333333333333333,
self::MED => 1.333333333333333,
self::HIGH => 3,
);
protected static $offset = array(
self::LOW => 0,
self::MED => -0.666666666666667,
self::HIGH => -2,
);
/**#@-*/
/**
* @return integer 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 certainly, compute the adjusted fraction.
* @param number $fraction the raw fraction for this question.
* @param integer $certainty one of the certainly level constants.
* @return number the adjusted fraction taking the certainly into account.
*/
public static function adjust_fraction($fraction, $certainty) {
return self::$offset[$certainty] + self::$factor[$certainty] * $fraction;
}
/**
* @param integer $certainty one of the LOW/MED/HIGH constants.
* @return string a textual desciption of this certainly.
*/
public static function get_string($certainty) {
return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
}
public static function summary_with_certainty($summary, $certainty) {
if (is_null($certainty)) {
return $summary;
}
return $summary . ' [' . self::get_string($certainty) . ']';
}
}

View File

@ -0,0 +1,125 @@
<?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
* certainly 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_deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../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 static function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');
}
public function get_min_fraction() {
return question_cbm::adjust_fraction(parent::get_min_fraction(), 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 $summary . ' [' . question_cbm::get_string(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($pendingstep) {
return parent::is_same_response($pendingstep) &&
$this->qa->get_last_behaviour_var('certainty') == $pendingstep->get_behaviour_var('certainty');
}
protected function is_complete_response($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;
}
public static function adjust_random_guess_score($fraction) {
return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
}
}

View File

@ -0,0 +1,9 @@
<?php
$string['assumingcertainty'] = 'You did not select a certainty. Assuming: {$a}.';
$string['certainty1'] = 'Not very (less than 67%%)';
$string['certainty2'] = 'Fairly (more than 67%%)';
$string['certainty3'] = 'Very (more than 85%%)';
$string['deferredcbm'] = 'Deferred feedback with CBM';
$string['howcertainareyou'] = 'How certain are you? $a';
$string['markadjustment'] = 'Based on the certainty you expressed, your base mark of {$a->rawmark} was adjusted to {$a->mark}.';

View File

@ -0,0 +1,87 @@
<?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 deferred
* feedback behaviour.
*
* @package qbehaviour_deferredcbm
* @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 certainly_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::empty_tag('input', $attributes) . ' ' .
html_writer::tag('label', question_cbm::get_string($certainty),
array('for' => $id));
}
return $choices;
}
public function controls(question_attempt $qa, question_display_options $options) {
return html_writer::tag('div', get_string('howcertainareyou', 'qbehaviour_deferredcbm',
$this->certainly_choices($qa->get_behaviour_field_name('certainty'),
$qa->get_last_behaviour_var('certainty'), $options->readonly)),
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')) {
$feedback .= html_writer::tag('p', get_string('assumingcertainty', 'qbehaviour_deferredcbm',
question_cbm::get_string($qa->get_last_behaviour_var('_assumedcertainty'))));
}
if ($options->marks) {
$a->rawmark = format_float(
$qa->get_last_behaviour_var('_rawfraction') * $qa->get_max_mark(), $options->markdp);
$a->mark = $qa->format_mark($options->markdp);
$feedback .= html_writer::tag('p', get_string('markadjustment', 'qbehaviour_deferredcbm', $a));
}
return $feedback;
}
}

View File

@ -0,0 +1,261 @@
<?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 file contains tests that walks a question through the deferred feedback
* behaviour.
*
* @package qbehaviour_deferredcbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_deferredcbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_deferred_cbm_truefalse_high_certainty() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredcbm', 2);
// Verify.
$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_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_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(2);
$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!', 1);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(new PatternExpectation('/' . 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->assertWithinMargin($autogradedstep->get_fraction(), -2, 0.0000001);
}
public function test_deferred_cbm_truefalse_low_certainty() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredcbm', 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());
// Submit ansewer with low certainty.
$this->process_submission(array('answer' => 1, '-certainty' => 1));
// Verify.
$this->check_current_state(question_state::$complete);
$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(0.6666667);
$this->check_current_output($this->get_contains_correct_expectation(),
$this->get_contains_cbm_radio_expectation(1, false, true));
$this->assertEqual(get_string('true', 'qtype_truefalse') . ' [' . question_cbm::get_string(1) . ']',
$this->quba->get_response_summary($this->slot));
}
public function test_deferred_cbm_truefalse_default_certainty() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredcbm', 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());
// 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(0.6666667);
$this->check_current_output($this->get_contains_correct_expectation(),
$this->get_contains_cbm_radio_expectation(1, false, false),
new PatternExpectation('/' . preg_quote(get_string('assumingcertainty', 'qbehaviour_deferredcbm',
question_cbm::get_string($qa->get_last_behaviour_var('_assumedcertainty')))) . '/'));
$this->assertEqual(get_string('true', 'qtype_truefalse'),
$this->quba->get_response_summary($this->slot));
}
public function test_deferredcbm_resume_multichoice_single() {
// 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', 3);
$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(-3.3333333);
$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->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertPattern('/' . preg_quote($mc->questiontext) . '/',
$this->quba->get_question_summary($this->slot));
$this->assertPattern('/(B|C) \[' . preg_quote(question_cbm::get_string(2)) . '\]/',
$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, 3);
$this->quba->start_question_based_on($this->slot, $oldqa);
// 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_cbm_radio_expectation(2, true, true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_does_not_contain_correctness_expectation());
$this->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertPattern('/' . 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(3, false, true),
$this->get_contains_correct_expectation());
$this->assertPattern('/(A) \[' . preg_quote(question_cbm::get_string(3)) . '\]/',
$this->quba->get_response_summary($this->slot));
}
public function test_deferred_cbm_truefalse_no_certainty_feedback_when_not_answered() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredcbm', 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 NoPatternExpectation('/class=\"im-feedback/'));
}
}

View File

@ -0,0 +1,94 @@
<?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_deferredfeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 {
const IS_ARCHETYPAL = true;
public function required_question_definition_type() {
return 'question_automatically_gradable';
}
public static function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');
}
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);
}
}
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;
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['deferredfeedback'] = 'Deferred feedback';

View File

@ -0,0 +1,30 @@
<?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 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 qbehaviour_deferredfeedback_renderer extends qbehaviour_renderer {
}

View File

@ -0,0 +1,205 @@
<?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 file contains tests that walks a question through 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_deferredfeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_deferredfeedback_feedback_truefalse() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredfeedback', 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());
$this->assertEqual(get_string('true', 'qtype_truefalse'),
$this->quba->get_right_answer_summary($this->slot));
$this->assertPattern('/' . 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_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 PatternExpectation('/class="r0 correct"/'));
$this->assertEqual(get_string('true', 'qtype_truefalse'),
$this->quba->get_response_summary($this->slot));
// Process a manual comment.
$this->manual_grade('Not good enough!', 1);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(1);
$this->check_current_output(
new PatternExpectation('/' . 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->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
}
public function test_deferredfeedback_feedback_multichoice_single() {
// 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_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_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() {
// 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->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::$todo);
$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());
}
}

View File

@ -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/>.
/**
* Question behaviour where the student can submit questions one at a
* time for immediate feedback, with certainty based marking.
*
* @package qbehaviour_immediatecbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../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 certainly, 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 certainly 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 {
const IS_ARCHETYPAL = true;
public function get_min_fraction() {
return question_cbm::adjust_fraction(parent::get_min_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($pendingstep) {
return parent::is_same_response($pendingstep) &&
$this->qa->get_last_behaviour_var('certainty') == $pendingstep->get_behaviour_var('certainty');
}
protected function is_complete_response($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->qa->get_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;
}
public static function adjust_random_guess_score($fraction) {
return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
}
}

View File

@ -0,0 +1,4 @@
<?php
$string['immediatecbm'] = 'Immediate feedback with CBM';
$string['pleaseselectacertainty'] = 'Please select a certainty.';

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/>.
/**
* Renderer for outputting parts of a question belonging to the immediate
* feedback with CBM behaviour.
*
* @package qbehaviour_immediatecbm
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../deferredcbm/renderer.php');
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;
}
}

View File

@ -0,0 +1,282 @@
<?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 file contains tests that walks a question through 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_immediatecbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_immediatecbm_feedback_multichoice_right() {
// 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->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
$this->quba->get_right_answer_summary($this->slot));
$this->assertPattern('/' . 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/3);
$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->assertEqual('A [' . question_cbm::get_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->assertEqual($numsteps, $this->get_step_count());
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2/3);
$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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_partcorrect_expectation(),
new PatternExpectation('/' . 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->assertWithinMargin($autogradedstep->get_fraction(), -10/9, 0.0000001);
}
public function test_immediatecbm_feedback_multichoice_try_to_submit_blank() {
// 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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_partcorrect_expectation(),
new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
}
public function test_immediatecbm_feedback_shortanswer_try_to_submit_no_certainty() {
// Create a short answer question with correct answer true.
$sa = test_question_maker::make_a_shortanswer_question();
$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(1);
$this->check_current_output(
$this->get_does_not_contain_validation_error_expectation());
}
public function test_immediatecbm_feedback_multichoice_wrong_on_finish() {
// 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(-3);
$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() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$this->start_attempt_at_question($tf, 'deferredcbm', 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 NoPatternExpectation('/class=\"im-feedback/'));
}
}

View File

@ -0,0 +1,131 @@
<?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_immediatefeedback
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 required_question_definition_type() {
return 'question_automatically_gradable';
}
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_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;
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['immediatefeedback'] = 'Immediate feedback';

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/>.
/**
* Renderer for outputting parts of a question belonging to 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 qbehaviour_immediatefeedback_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
return $this->submit_button($qa, $options);
}
}

View File

@ -0,0 +1,237 @@
<?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 file contains tests that walks a question through 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_immediatefeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_immediatefeedback_feedback_multichoice_right() {
// 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->assertEqual('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->assertEqual($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->assertEqual($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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_partcorrect_expectation(),
new PatternExpectation('/' . 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->assertWithinMargin($autogradedstep->get_fraction(), -0.3333333, 0.0000001);
}
public function test_immediatefeedback_feedback_multichoice_try_to_submit_blank() {
// 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);
// Verify.
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(0.5);
$this->check_current_output(
$this->get_contains_partcorrect_expectation(),
new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
}
public function test_immediatefeedback_feedback_multichoice_wrong_on_finish() {
// 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->assertPattern('/B|C/',
$this->quba->get_response_summary($this->slot));
}
}

View File

@ -0,0 +1,114 @@
<?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 informaiton items.
*
* @package qbehaviour_informationitem
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 required_question_definition_type() {
return 'question_definition';
}
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 Exception('Information items cannot be graded.');
}
return parent::process_comment($pendingstep);
}
public function process_finish(question_attempt_pending_step $pendingstep) {
$pendingstep->set_state(question_state::$finished);
return question_attempt::KEEP;
}
public function process_seen(question_attempt_pending_step $pendingstep) {
$pendingstep->set_state(question_state::$complete);
return question_attempt::KEEP;
}
}

View File

@ -0,0 +1,4 @@
<?php
$string['informationitem'] = 'behaviour for information items';
$string['seen'] = 'Seen';

View File

@ -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/>.
/**
* Renderer for outputting parts of a question belonging to the information
* item behaviour.
*
* @package qbehaviour_deferredfeedback
* @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 ($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,
));
}
}

View File

@ -0,0 +1,78 @@
<?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 file contains tests that walks a question through 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_informationitem_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_informationitem_feedback_description() {
// Create a true-false question with correct answer true.
$description = test_question_maker::make_a_description_question();
$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),
new ContainsTagWithAttributes('input', array('type' => 'hidden',
'name' => $this->quba->get_field_prefix($this->slot) . '-seen', 'value' => 1)),
$this->get_does_not_contain_feedback_expectation());
// Process a submission indicating this question has been seen.
$this->process_submission(array('-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 NoPatternExpectation('/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);
$this->check_current_state(question_state::$manfinished);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
// Check that trying to process a manual comment with a grade causes an exception.
$this->expectException();
$this->manual_grade('Not good enough!', 1);
}
}

View File

@ -0,0 +1,243 @@
<?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_interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 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_interactive extends question_behaviour_with_save {
const IS_ARCHETYPAL = true;
/**
* Special value used for {@link question_display_options::$readonly when
* we are showing the try again button to the student during an attempt.
* The particular number was chosen randomly. PHP will treat it the same
* as true, but in the renderer we reconginse it display the try again
* button enabled even though the rest of the question is disabled.
* @var integer
*/
const READONLY_EXCEPT_TRY_AGAIN = 23485299;
public function required_question_definition_type() {
return 'question_automatically_gradable';
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
/**
* @return boolean are we are currently in the try_again state.
*/
protected 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);
return;
}
// 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;
// In a try-again state, everything except the try again button
// Should be read-only. This is a mild hack to achieve this.
if (!$options->readonly) {
$options->readonly = self::READONLY_EXCEPT_TRY_AGAIN;
}
}
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);
}
if ($this->is_try_again_state()) {
return get_string('notcomplete', 'qbehaviour_interactive');
} else {
return get_string('triesremaining', 'qbehaviour_interactive',
$this->qa->get_last_behaviour_var('_triesleft'));
}
}
public function init_first_step(question_attempt_step $step) {
parent::init_first_step($step);
$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;
}
}

View File

@ -0,0 +1,6 @@
<?php
$string['interactive'] = 'Interactive with multiple tries';
$string['notcomplete'] = 'Not complete';
$string['triesremaining'] = 'Tries remaining: $a';
$string['tryagain'] = 'Try again';

View File

@ -0,0 +1,62 @@
<?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_interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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) {
return $this->submit_button($qa, $options);
}
public function feedback(question_attempt $qa, question_display_options $options) {
if (!$qa->get_state()->is_active() || !$options->readonly) {
return '';
}
$attributes = array(
'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',
);
if ($options->readonly !== qbehaviour_interactive::READONLY_EXCEPT_TRY_AGAIN) {
$attributes['disabled'] = 'disabled';
}
$output = html_writer::empty_tag('input', $attributes);
if (empty($attributes['disabled'])) {
$output .= print_js_call('question_init_submit_button',
array($attributes['id'], $qa->get_slot()), true);
}
return $output;
}
}

View File

@ -0,0 +1,470 @@
<?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 file contains tests that walks a question through the interactive
* behaviour.
*
* @package qbehaviour_interactive
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_interactive_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_interactive_feedback_multichoice_right() {
// Create a multichoice single question.
$mc = test_question_maker::make_a_multichoice_single_question();
$mc->hints = array(
new question_hint_with_parts('This is the first hint.', false, false),
new question_hint_with_parts('This is the second hint.', 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_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
$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_contains_submit_button_expectation(false),
$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->assertEqual($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);
// 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 PatternExpectation('/' . 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->assertWithinMargin($autogradedstep->get_fraction(), 0.6666667, 0.0000001);
}
public function test_interactive_finish_when_try_again_showing() {
// Create a multichoice single question.
$mc = test_question_maker::make_a_multichoice_single_question();
$mc->hints = array(
new question_hint_with_parts('This is the first hint.', 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 PatternExpectation('/' . 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_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
$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() {
// Create a short answer question.
$sa = test_question_maker::make_a_shortanswer_question();
$sa->hints = array(
new question_hint('This is the first hint.'),
new question_hint('This is the second hint.'),
);
$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_contains_submit_button_expectation(false),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_contains_try_again_button_expectation(true),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
$this->get_contains_hint_expectation('This is the first hint'));
$this->assertEqual('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_contains_submit_button_expectation(false),
$this->get_contains_correct_expectation(),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_no_hint_visible_expectation());
$this->assertEqual('frog',
$this->quba->get_response_summary($this->slot));
}
public function test_interactive_feedback_multichoice_multiple_reset() {
// Create a multichoice multiple question.
$mc = test_question_maker::make_a_multichoice_multi_question();
$mc->hints = array(
new question_hint_with_parts('This is the first hint.', true, true),
new question_hint_with_parts('This is the second hint.', 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 PatternExpectation('/' . 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_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
$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() {
// Create a multichoice multiple question.
$q = test_question_maker::make_a_shortanswer_question();
$q->hints = array(
new question_hint_with_parts('This is the first hint.', true, true),
new question_hint_with_parts('This is the second hint.', 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[0]->fraction = 0.6666667;
$q->answers[1]->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() {
// Create a multichoice multiple question.
$q = test_question_maker::make_a_shortanswer_question();
$q->hints = array(
new question_hint_with_parts('This is the first hint.', true, true),
new question_hint_with_parts('This is the second hint.', 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[0]->fraction = 0.6666667;
$q->answers[1]->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);
}
}

View File

@ -0,0 +1,91 @@
<?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_interactivecountback
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../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 acutally 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 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactivecountback extends qbehaviour_interactive {
const IS_ARCHETYPAL = false;
public function required_question_definition_type() {
return '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);
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['interactivecountback'] = 'Interactive with multiple tries (credit for earlier tries)';

View File

@ -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/>.
require_once(dirname(__FILE__) . '/../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.
*
* @package qbehaviour_interactivecountback
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_interactivecountback_renderer extends qbehaviour_interactive_renderer {
}

View File

@ -0,0 +1,135 @@
<?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 file contains tests that walks a question through the interactive with
* countback behaviour.
*
* @package qbehaviour_interactivecountback
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_interactivecountback_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_interactive_feedback_match_reset() {
// Create a matching question.
$m = test_question_maker::make_a_matching_question();
$m->shufflestems = false;
$m->hints = array(
new question_hint_with_parts('This is the first hint.', true, true),
new question_hint_with_parts('This is the second hint.', 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 = array(0 => get_string('choose') . '...');
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->assertEqual('interactivecountback',
$this->quba->get_question_attempt($this->slot)->get_behaviour_name());
$this->check_current_output(
$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),
$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());
// 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_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),
$this->get_contains_submit_button_expectation(false),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
$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]));
// Check that extract responses will return the reset data.
$prefix = $this->quba->get_field_prefix($this->slot);
$this->assertEqual(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_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),
$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('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_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),
$this->get_contains_submit_button_expectation(false),
$this->get_does_not_contain_try_again_button_expectation(),
$this->get_contains_correct_expectation(),
$this->get_contains_standard_correct_combined_feedback_expectation(),
new NoPatternExpectation('/class="control\b[^"]*\bpartiallycorrect"/'));
}
}

View File

@ -0,0 +1,92 @@
<?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_manualgraded
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 {
const IS_ARCHETYPAL = true;
public static function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');
}
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);
}
}
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_complete_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;
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['manualgraded'] = 'Manually graded';

View File

@ -0,0 +1,31 @@
<?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 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 qbehaviour_manualgraded_renderer extends qbehaviour_renderer {
}

View File

@ -0,0 +1,259 @@
<?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 file contains tests that walks a question through 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_test_base {
public function test_manual_graded_essay() {
// 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->assertEqual('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!'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new ContainsTagWithAttribute('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!'));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
$this->process_submission(array('answer' => ''));
$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!'));
$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->assertEqual('This is my wonderful essay!',
$this->quba->get_response_summary($this->slot));
// Process a manual comment.
$this->manual_grade('Not good enough!', 10);
// Verify.
$this->check_current_state(question_state::$mangrright);
$this->check_current_mark(10);
$this->check_current_output(
new PatternExpectation('/' . 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_truefalse() {
// Create a true-false question with correct answer true.
$tf = test_question_maker::make_a_truefalse_question();
$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);
$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 PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
}
public function test_manual_graded_ignore_repeat_sumbission() {
// 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->assertEqual('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!'));
// 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->assertEqual('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('', '');
$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.', '');
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/' . preg_quote('I am not sure what grade to award.') . '/'));
// Now grade it.
$this->manual_grade('Pretty good!', '9.00000');
$this->check_step_count($numsteps + 2);
$this->check_current_state(question_state::$mangrpartial);
$this->check_current_mark(9);
$this->check_current_output(
new PatternExpectation('/' . 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');
$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.', '');
$this->check_step_count($numsteps + 3);
$this->check_current_state(question_state::$needsgrading);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/' . preg_quote('Actually, I am not sure any more.') . '/'));
$qa = $this->quba->get_question_attempt($this->slot);
$this->assertEqual('Commented: Actually, I am not sure any more.',
$qa->summarise_action($qa->get_last_step()));
}
public function test_manual_graded_essay_can_grade_0() {
// 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->assertEqual('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!'));
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->check_current_output(
new ContainsTagWithAttribute('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->assertEqual('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);
// Verify.
$this->check_current_state(question_state::$mangrwrong);
$this->check_current_mark(0);
}
}

View File

@ -0,0 +1,65 @@
<?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 qim was not
* available.
*
* @package qbehaviour_missing
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* 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 an 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 required_question_definition_type() {
return 'question_definition';
}
public function summarise_action(question_attempt_step $step) {
return '';
}
public function init_first_step(question_attempt_step $step) {
throw new 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 Exception('The behaviour used for this question is not available. No processing is possible.');
}
public function get_min_fraction() {
throw new Exception('The behaviour used for this question is not available. No processing is possible.');
}
}

View File

@ -0,0 +1,3 @@
<?php
$string['questionusedunknownmodel'] = 'This question was attempted with an behaviour that is not currently availalbe. We are attempting to show the question, but there may be problems.';

View File

@ -0,0 +1,35 @@
<?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 when the actual behaviour
* used is not available.
*
* @package qbehaviour_missing
* @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'));
}
}

View File

@ -0,0 +1,96 @@
<?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 file contains 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
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
require_once(dirname(__FILE__) . '/../behaviour.php');
class qbehaviour_missing_test extends UnitTestCase {
public function test_missing_cannot_start() {
$qa = new question_attempt(test_question_maker::make_a_truefalse_question(), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException();
$behaviour->init_first_step(new question_attempt_step(array()));
}
public function test_missing_cannot_process() {
$qa = new question_attempt(test_question_maker::make_a_truefalse_question(), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException();
$behaviour->process_action(new question_attempt_pending_step(array()));
}
public function test_missing_cannot_get_min_grade() {
$qa = new question_attempt(test_question_maker::make_a_truefalse_question(), 0);
$behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
$this->expectException();
$behaviour->get_min_fraction();
}
public function test_render_missing() {
$records = testing_db_record_builder::build_db_records(array(
array('id', 'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'maxmark', 'minfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, 1, 1, 1, 'strangeunknown', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, '_order', '1,2,3'),
array(2, 1, 1, 1, 'strangeunknown', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, '-submit', '1'),
array(3, 1, 1, 1, 'strangeunknown', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, 'choice0', '1'),
));
$question = test_question_maker::make_a_truefalse_question();
$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->assertEqual(2, $qa->get_num_steps());
$step = $qa->get_step(0);
$this->assertEqual(question_state::$todo, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233700, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('_order' => '1,2,3'), $step->get_all_data());
$step = $qa->get_step(1);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertEqual(0.5, $step->get_fraction());
$this->assertEqual(1256233705, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('-submit' => '1', 'choice0' => '1'), $step->get_all_data());
$output = $qa->render(new question_display_options(), '1');
$this->assertPattern('/' . preg_quote($qa->get_question()->questiontext) . '/', $output);
$this->assertPattern('/' . preg_quote(get_string('questionusedunknownmodel', 'qbehaviour_missing')) . '/', $output);
$this->assert(new ContainsTagWithAttribute('div', 'class', 'warning'), $output);
}
}

View File

@ -0,0 +1,197 @@
<?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 that is used when the actual qim was not
* available.
*
* @package qbehaviour_opaque
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* This behaviour is specifically for use with the Opaque question type.
*
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_opaque extends question_behaviour {
/** @var string */
protected $preferredbehaviour;
/** @var string */
protected $questionsummary;
public function __construct(question_attempt $qa, $preferredbehaviour) {
parent::__construct($qa, $preferredbehaviour);
$this->preferredbehaviour = $preferredbehaviour;
}
public function required_question_definition_type() {
return 'qtype_opaque_question';
}
public function get_state_string($showcorrectness) {
$state = $this->qa->get_state();
$omstate = $this->qa->get_last_behaviour_var('_statestring');
if ($state->is_finished()) {
return $state->default_string($showcorrectness);
} else if ($omstate) {
return $omstate;
} else {
return get_string('notcomplete', 'qbehaviour_opaque');
}
}
public function init_first_step(question_attempt_step $step) {
global $USER;
if ($step->has_behaviour_var('_randomseed')) {
// Reinitialising, nothing to do.
return;
}
// Set up the random seed to be the current time in milliseconds.
list($micros, $sec) = explode(" ", microtime());
$step->set_behaviour_var('_randomseed', $sec . floor($micros * 1000));
$step->set_behaviour_var('_userid', $USER->id);
$step->set_behaviour_var('_language', current_language());
$step->set_behaviour_var('_preferredbehaviour', $this->preferredbehaviour);
$opaquestate = update_opaque_state($this->qa, $step);
$step->set_behaviour_var('_statestring', $opaquestate->progressinfo);
// Remember the question summary.
$this->questionsummary = html_to_text($opaquestate->xhtml, 0, false);
}
public function get_question_summary() {
return $this->questionsummary;
}
protected function is_same_response(question_attempt_step $pendingstep) {
$newdata = $pendingstep->get_submitted_data();
foreach ($newdata as $key => $ignored) {
// If an omact_ button has been clicked, never treat this as a duplicate submission.
if (strpos($key, 'omact_') === 0) {
return false;
}
}
$olddata = $this->qa->get_last_step()->get_submitted_data();
return question_utils::arrays_have_same_keys_and_values($newdata, $olddata);
}
public function summarise_action(question_attempt_step $step) {
if ($step->has_behaviour_var('finish')) {
return $this->summarise_finish($step);
} else if ($step->has_behaviour_var('comment')) {
return $this->summarise_manual_comment($step);
} else {
$data = $step->get_qt_data();
$formatteddata = array();
foreach ($data as $name => $value) {
if (substr($name, 0, 1) == '_') {
continue;
}
$formatteddata[] = $name . ' => ' . s($value);
}
if ($formatteddata) {
return get_string('submitted', 'question', implode(', ', $formatteddata));
} else {
return $this->summarise_start($step);
}
}
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
}
if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($this->is_same_response($pendingstep) ||
$this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
} else {
return $this->process_remote_action($pendingstep);
}
}
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
// They tried to finish the usage without having finished this question.
// That is, they gave up.
$pendingstep->set_state(question_state::$gaveup);
return question_attempt::KEEP;
}
public function process_remote_action(question_attempt_pending_step $pendingstep) {
$opaquestate = update_opaque_state($this->qa, $pendingstep);
if (is_string($opaquestate)) {
notify($opaquestate);
return question_attempt::DISCARD; // TODO
}
if ($opaquestate->resultssequencenumber != $this->qa->get_num_steps()) {
$pendingstep->set_state(question_state::$todo);
$pendingstep->set_behaviour_var('_statestring', $opaquestate->progressinfo);
} else {
// Look for a score on the default axis.
$pendingstep->set_fraction(0);
foreach ($opaquestate->results->scores as $score) {
if ($score->axis == '') {
$pendingstep->set_fraction($score->marks / $this->question->defaultmark);
}
}
if ($opaquestate->results->attempts > 0) {
$pendingstep->set_state(question_state::$gradedright);
} else {
$pendingstep->set_state(
question_state::graded_state_for_fraction($pendingstep->get_fraction()));
}
if (!empty($opaquestate->results->questionLine)) {
$this->qa->set_question_summary(
$this->cleanup_results($opaquestate->results->questionLine));
}
if (!empty($opaquestate->results->answerLine)) {
$pendingstep->set_new_response_summary(
$this->cleanup_results($opaquestate->results->answerLine));
}
if (!empty($opaquestate->results->actionSummary)) {
$pendingstep->set_behaviour_var('_actionsummary',
$this->cleanup_results($opaquestate->results->actionSummary));
}
}
return question_attempt::KEEP;
}
protected function cleanup_results($line) {
return preg_replace('/\\s+/', ' ', $line);
}
}

View File

@ -0,0 +1,4 @@
<?php
$string['notcomplete'] = 'Not complete';
$string['opaque'] = 'Question managed by a remote engine';

View File

@ -0,0 +1,65 @@
<?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 when the actual behaviour
* used is not available.
*
* @package qbehaviour_opaque
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_opaque_renderer extends qbehaviour_renderer {
public function controls(question_attempt $qa, question_display_options $options) {
if ($qa->get_state()->is_gave_up()) {
return html_writer::tag('div', get_string('notcompletedmessage', 'qtype_opaque'),
array('class' => 'question_aborted'));
}
$opaquestate =& update_opaque_state($qa);
if (is_string($opaquestate)) {
return notify($opaquestate, '', '', true);
// TODO
}
return html_writer::tag('div', $opaquestate->xhtml,
array('class' => opaque_browser_type()));
}
public function head_code(question_attempt $qa) {
$output = '';
$opaquestate =& update_opaque_state($qa);
$question = $qa->get_question();
$resourcecache = new opaque_resource_cache($question->engineid,
$question->remoteid, $question->remoteversion);
if (!empty($opaquestate->cssfilename) && $resourcecache->file_in_cache($opaquestate->cssfilename)) {
$output .= '<link rel="stylesheet" type="text/css" href="' .
$resourcecache->file_url($opaquestate->cssfilename) . '" />';
}
if(!empty($opaquestate->headXHTML)) {
$output .= $opaquestate->headXHTML;
}
return $output;
}
}

View File

@ -0,0 +1,227 @@
<?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 file contains tests for the Opaque behaviour.
*
* @package qbehaviour_opaque
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../engine/lib.php');
require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
require_once(dirname(__FILE__) . '/../behaviour.php');
class qbehaviour_opaque_test extends qbehaviour_walkthrough_test_base {
/**
* Makes an Opaque question that refers to one of the sample questions
* supplied by OpenMark.
* @return unknown_type
*/
protected function make_standard_om_question() {
$engineid = get_field('question_opaque_engines', 'MIN(id)', '', '');
if (empty($engineid)) {
throw new Exception('Cannot test Opaque. No question engines configured.');
}
question_bank::load_question_definition_classes('opaque');
$q = new qtype_opaque_question();
test_question_maker::initialise_a_question($q);
$q->name = 'samples.mu120.module5.question01';
$q->qtype = question_bank::get_qtype('opaque');
$q->defaultmark = 3;
$q->engineid = $engineid;
$q->remoteid = 'samples.mu120.module5.question01';
$q->remoteversion = '1.0';
return $q;
}
public function test_wrong_three_times() {
$q = $this->make_standard_om_question();
$this->start_attempt_at_question($q, 'interactive');
$qa = $this->quba->get_question_attempt($this->slot);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/You have 3 attempts/'),
$this->get_contains_button_expectation($qa->get_qt_field_name('omact_gen_14'), 'Check'));
$this->assertPattern('/^\s*Below is a plan of a proposed garden./',
$qa->get_question_summary());
$this->assertNull($qa->get_right_answer_summary());
// Submit a wrong answer.
$this->process_submission(array('omval_response1' => 1, 'omval_response2' => 666,
'omact_gen_14' => 'Check'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/incorrect/'),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_opaque')) . '/'),
$this->get_contains_button_expectation($qa->get_qt_field_name('omact_ok'), 'Try again'));
// Try again.
$this->process_submission(array('omact_ok' => 'Try again'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/You have 2 attempts/'));
// Submit a wrong answer again.
$this->process_submission(array('omval_response1' => 1, 'omval_response2' => 666,
'omact_gen_14' => 'Check'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/still incorrect/'),
new PatternExpectation('/' . preg_quote(get_string('notcomplete', 'qbehaviour_opaque')) . '/'));
// Try again.
$this->process_submission(array('omact_ok' => 'Try again'));
// Verify.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/This is your last attempt/'));
// Submit a wrong answer third time.
$this->process_submission(array('omval_response1' => 1, 'omval_response2' => 666,
'omact_gen_14' => 'Check'));
// Verify.
$this->check_current_state(question_state::$gradedwrong);
$this->check_current_mark(0);
$this->check_current_output(
new PatternExpectation('/Please see MU120 Preparatory Resource Book B section 5.1/'),
new PatternExpectation('/still incorrect/'));
$this->assertTrue(preg_match('/What is \(X\*W\) (\d+\.\d+)\*(\d+), \(X\*L\)(\d+\.\d+)\*(\d+)\?/',
$qa->get_question_summary(), $matches));
$this->assertNull($qa->get_right_answer_summary());
$this->assertPattern('/' . $matches[1]*$matches[2] . '.*, ' . $matches[3]*$matches[4] . '/',
$qa->get_response_summary());
}
public function test_right_first_time() {
$q = $this->make_standard_om_question();
$this->start_attempt_at_question($q, 'interactive');
$qa = $this->quba->get_question_attempt($this->slot);
// Work out right answer (yuck!)
$html = $this->quba->render_question($this->slot, $this->displayoptions);
preg_match('/(0\.5|2\.0|3\.0) metres/', $html, $matches);
$scale = $matches[1];
preg_match('/Patio|Summer House|Flowerbed|Vegetable Plot|Pond/', $html, $matches);
$feature = $matches[0];
$sizes = array(
'Patio' => array(4, 7),
'Summer House' => array(3, 5),
'Flowerbed' => array(2, 7),
'Vegetable Plot' => array(3, 10),
'Pond' => array(2, 3),
);
$size = $sizes[$feature];
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/You have 3 attempts/'),
$this->get_contains_button_expectation($qa->get_qt_field_name('omact_gen_14'), 'Check'));
// Submit the right answer.
$this->process_submission(array('omval_response1' => $size[0] * $scale,
'omval_response2' => $size[1] * $scale, 'omact_gen_14' => 'Check'));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(3);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/correct/'));
}
public function test_different_max() {
$q = $this->make_standard_om_question();
$this->start_attempt_at_question($q, 'interactive', 6.0);
$qa = $this->quba->get_question_attempt($this->slot);
// Work out right answer (yuck!)
$html = $this->quba->render_question($this->slot, $this->displayoptions);
preg_match('/(0\.5|2\.0|3\.0) metres/', $html, $matches);
$scale = $matches[1];
preg_match('/Patio|Summer House|Flowerbed|Vegetable Plot|Pond/', $html, $matches);
$feature = $matches[0];
$sizes = array(
'Patio' => array(4, 7),
'Summer House' => array(3, 5),
'Flowerbed' => array(2, 7),
'Vegetable Plot' => array(3, 10),
'Pond' => array(2, 3),
);
$size = $sizes[$feature];
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/You have 3 attempts/'),
$this->get_contains_button_expectation($qa->get_qt_field_name('omact_gen_14'), 'Check'));
// Submit the right answer.
$this->process_submission(array('omval_response1' => $size[0] * $scale,
'omval_response2' => $size[1] * $scale, 'omact_gen_14' => 'Check'));
// Verify.
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(6);
$this->check_current_output(
new PatternExpectation('/Below is a plan of a proposed garden/'),
new PatternExpectation('/correct/'));
}
public function test_gave_up() {
$q = $this->make_standard_om_question();
$this->start_attempt_at_question($q, 'interactive');
$this->quba->finish_all_questions();
$this->check_current_state(question_state::$gaveup);
$this->check_current_mark(null);
$this->check_current_output(
new PatternExpectation('/' .
preg_quote(get_string('notcompletedmessage', 'qtype_opaque')) . '/'));
}
}

View File

@ -0,0 +1,200 @@
<?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
*/
/**
* 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 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) {
$commentfield = $qa->get_behaviour_field_name('comment');
$comment = print_textarea(can_use_html_editor(), 10, 80, null, null, $commentfield, $qa->get_manual_comment(), 0, true);
$comment = html_writer::tag('div', html_writer::tag('div',
html_writer::tag('label', get_string('comment', 'question'), array('for' => $commentfield)),
array('class' => 'fitemtitle')) .
html_writer::tag('div', $comment, array('class' => 'felement fhtmleditor')),
array('class' => 'fitem'));
$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,
);
if (!is_null($currentmark)) {
$attributes['value'] = $qa->format_fraction_as_mark($currentmark / $maxmark, $options->markdp);
}
$a = new stdClass;
$a->max = $qa->format_max_mark($options->markdp);
$a->mark = html_writer::empty_tag('input', $attributes);
$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(),
));
$errorclass = '';
$error = '';
if ($currentmark > $maxmark || $currentmark < $maxmark * $qa->get_min_fraction()) {
$errorclass = ' error';
$error = html_writer::tag('span', get_string('manualgradeoutofrange', 'question'),
array('class' => 'error')) . html_writer::empty_tag('br');
}
$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()->format_comment());
}
if ($options->manualcommentlink) {
$strcomment = get_string('commentormark', 'question');
$link = link_to_popup_window($options->manualcommentlink .
'&amp;slot=' . $qa->get_slot(),
'commentquestion', $strcomment, 600, 800, $strcomment, 'none', true);
$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) {
$attributes = array(
'type' => 'submit',
'id' => $qa->get_behaviour_field_name('submit'),
'name' => $qa->get_behaviour_field_name('submit'),
'value' => get_string('check', 'question'),
'class' => 'submit btn',
);
if ($options->readonly) {
$attributes['disabled'] = 'disabled';
}
$output = html_writer::empty_tag('input', $attributes);
if (!$options->readonly) {
$output .= print_js_call('question_init_submit_button',
array($attributes['id'], $qa->get_slot()), true);
}
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 '';
}
}

View File

@ -1,37 +1,34 @@
<?php
///////////////////////////////////////////////////////////////////////////
// //
// NOTICE OF COPYRIGHT //
// //
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
// http://moodle.org //
// //
// Copyright (C) 1999 onwards Martin Dougiamas and others //
// //
// This program 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 2 of the License, or //
// (at your option) any later version. //
// //
// This program 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: //
// //
// http://www.gnu.org/copyleft/gpl.html //
// //
///////////////////////////////////////////////////////////////////////////
// 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/>.
/**
* Functions used to show question editing interface
*
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package questionbank
*//** */
* @package moodlecore
* @subpackage questionbank
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->libdir.'/questionlib.php');
define('DEFAULT_QUESTIONS_PER_PAGE', 20);
function get_module_from_cmid($cmid) {
@ -1428,10 +1425,10 @@ class question_bank_view {
foreach ($questionlist as $questionid) {
$questionid = (int)$questionid;
question_require_capability_on($questionid, 'edit');
if ($DB->record_exists('quiz_question_instances', array('question' => $questionid))) {
if (questions_in_use(array($questionid))) {
$DB->set_field('question', 'hidden', 1, array('id' => $questionid));
} else {
delete_question($questionid);
question_delete_question($questionid);
}
}
}
@ -1464,7 +1461,7 @@ class question_bank_view {
$key = $matches[1];
$questionlist .= $key.',';
question_require_capability_on($key, 'edit');
if ($DB->record_exists('quiz_question_instances', array('question' => $key))) {
if (questions_in_use(array($key))) {
$questionnames .= '* ';
$inuse = true;
}

221
question/engine/bank.php Normal file
View File

@ -0,0 +1,221 @@
<?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/>.
/**
* More object oriented wrappers around parts of the Moodle question bank.
*
* In due course, I expect that the question bank will be converted to a
* fully object oriented structure, at which point this file can be a
* starting point.
*
* @package moodlecore
* @subpackage questionbank
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* This static class provides access to the other question bank.
*
* It provides functions for managing question types and question definitions.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_bank {
/** @var array question type name => question_type subclass. */
private static $questiontypes = array();
/** @var array question type name => 1. Records which question definitions have been loaded. */
private static $loadedqdefs = array();
protected static $questionfinder = null;
/** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
private static $testmode = false;
private static $testdata = array();
/**
* Get the question type class for a particular question type.
* @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
* @param boolean $mustexist if false, the missing question type is returned when
* the requested question type is not installed.
* @return question_type the corresponding question type class.
*/
public static function get_qtype($qtypename, $mustexist = true) {
global $CFG;
if (isset(self::$questiontypes[$qtypename])) {
return self::$questiontypes[$qtypename];
}
$file = $CFG->dirroot . '/question/type/' . $qtypename . '/questiontype.php';
if (!is_readable($file)) {
if ($mustexist || $qtypename == 'missingtype') {
throw new Exception('Unknown question type ' . $qtypename);
} else {
return self::get_qtype('missingtype');
}
}
include_once($file);
$class = 'qtype_' . $qtypename;
self::$questiontypes[$qtypename] = new $class();
return self::$questiontypes[$qtypename];
}
/**
* @param $qtypename the internal name of a question type, for example multichoice.
* @return string the human_readable name of this question type, from the language pack.
*/
public static function get_qtype_name($qtypename) {
return self::get_qtype($qtypename)->menu_name();
}
/**
* @return array all the installed question types.
*/
public static function get_all_qtypes() {
$qtypes = array();
$plugins = get_list_of_plugins('question/type', 'datasetdependent');
foreach ($plugins as $plugin) {
$qtypes[$plugin] = self::get_qtype($plugin);
}
return $qtypes;
}
/**
* Load the question definition class(es) belonging to a question type. That is,
* include_once('/question/type/' . $qtypename . '/question.php'), with a bit
* of checking.
* @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
*/
public static function load_question_definition_classes($qtypename) {
global $CFG;
if (isset(self::$loadedqdefs[$qtypename])) {
return;
}
$file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
if (!is_readable($file)) {
throw new Exception('Unknown question type (no definition) ' . $qtypename);
}
include_once($file);
self::$loadedqdefs[$qtypename] = 1;
}
/**
* Load a question definition from the database. The object returned
* will actually be of an appropriate {@link question_definition} subclass.
* @param integer $questionid the id of the question to load.
* @return question_definition loaded from the database.
*/
public static function load_question($questionid) {
if (self::$testmode) {
// Evil, test code in production, but now way round it.
return self::return_test_question_data($questionid);
}
$questiondata = get_record('question', 'id', $questionid);
if (empty($questiondata)) {
throw new Exception('Unknown question id ' . $questionid);
}
get_question_options($questiondata);
return self::make_question($questiondata);
}
/**
* Convert the question information loaded with {@link get_question_options()}
* to a question_definintion object.
* @param object $questiondata raw data loaded from the database.
* @return question_definition loaded from the database.
*/
public static function make_question($questiondata) {
return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
}
/**
* @return question_finder a question finder.
*/
public static function get_finder() {
if (is_null(self::$questionfinder)) {
self::$questionfinder = new question_finder();
}
return self::$questionfinder;
}
/**
* Only to be called from unit tests. Allows {@link load_test_data()} to be used.
*/
public static function start_unit_test() {
self::$testmode = true;
}
/**
* Only to be called from unit tests. Allows {@link load_test_data()} to be used.
*/
public static function end_unit_test() {
self::$testmode = false;
self::$testdata = array();
}
private static function return_test_question_data($questionid) {
if (!isset(self::$testdata[$questionid])) {
throw new Exception('question_bank::return_test_data(' . $questionid .
') called, but no matching question has been loaded by load_test_data.');
}
return self::$testdata[$questionid];
}
/**
* To be used for unit testing only. Will throw an exception if
* {@link start_unit_test()} has not been called first.
* @param object $questiondata a question data object to put in the test data store.
*/
public static function load_test_question_data(question_definition $question) {
if (!self::$testmode) {
throw new Exception('question_bank::load_test_data called when not in test mode.');
}
self::$testdata[$question->id] = $question;
}
}
class question_finder {
/**
* Get the ids of all the questions in a list of categoryies.
* @param integer|string|array $categoryids either a categoryid, or a comma-separated list
* category ids, or an array of them.
* @param string $extraconditions extra conditions to AND with the rest of the where clause.
* @return array questionid => questionid.
*/
public function get_questions_from_categories($categoryids, $extraconditions) {
if (is_array($categoryids)) {
$categoryids = implode(',', $categoryids);
}
if ($extraconditions) {
$extraconditions = ' AND (' . $extraconditions . ')';
}
$questionids = get_records_select_menu('question',
"category IN ($categoryids)
AND parent = 0
AND hidden = 0
$extraconditions", '', 'id,id AS id2');
if (!$questionids) {
$questionids = array();
}
return $questionids;
}
}

1101
question/engine/datalib.php Normal file

File diff suppressed because it is too large Load Diff

2877
question/engine/lib.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,362 @@
<?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/>.
/**
* Renderers for outputting parts of the question engine.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* This renderer controls the overall output of questions. It works with a
* {@link qbehaviour_renderer} and a {@link qtype_renderer} to output the
* type-specific bits. The main entry point is the {@link question()} method.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_question_renderer extends renderer_base {
/**
* Generate the display of a question in a particular state, and with certain
* display options. Normally you do not call this method directly. Intsead
* you call {@link question_usage_by_activity::render_question()} which will
* call this method with appropriate arguments.
*
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param qtype_renderer $qtoutput the renderer to output the question type
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @param string|null $number The question number to display. 'i' is a special
* value that gets displayed as Information. Null means no number is displayed.
* @return string HTML representation of the question.
*/
public function question(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options, $number) {
$output = '';
$output .= html_writer::start_tag('div', array(
'id' => 'q' . $qa->get_slot(),
'class' => 'que ' . $qa->get_question()->qtype->name() . ' ' .
$qa->get_behaviour_name(),
));
$output .= html_writer::tag('div',
$this->info($qa, $behaviouroutput, $qtoutput, $options, $number),
array('class' => 'info'));
$output .= html_writer::start_tag('div', array('class' => 'content'));
$output .= html_writer::tag('div',
$this->add_part_heading(get_string('questiontext', 'question'),
$this->formulation($qa, $behaviouroutput, $qtoutput, $options)),
array('class' => 'formulation'));
$output .= html_writer::nonempty_tag('div',
$this->add_part_heading(get_string('feedback', 'question'),
$this->outcome($qa, $behaviouroutput, $qtoutput, $options)),
array('class' => 'outcome'));
$output .= html_writer::nonempty_tag('div',
$this->add_part_heading(get_string('comments', 'question'),
$behaviouroutput->manual_comment($qa, $options)), array('class' => 'comment'));
$output .= html_writer::nonempty_tag('div',
$this->response_history($qa, $behaviouroutput, $qtoutput, $options),
array('class' => 'history'));
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('div');
return $output;
}
/**
* Generate the information bit of the question display that contains the
* metadata like the question number, current state, and mark.
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param qtype_renderer $qtoutput the renderer to output the question type
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @param string|null $number The question number to display. 'i' is a special
* value that gets displayed as Information. Null means no number is displayed.
* @return HTML fragment.
*/
protected function info(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options, $number) {
$output = '';
$output .= $this->number($number);
$output .= $this->status($qa, $behaviouroutput, $options);
$output .= $this->mark_summary($qa, $options);
$output .= $this->question_flag($qa, $options->flags);
return $output;
}
/**
* Generate the display of the question number.
* @param string|null $number The question number to display. 'i' is a special
* value that gets displayed as Information. Null means no number is displayed.
* @return HTML fragment.
*/
protected function number($number) {
$numbertext = '';
if (is_numeric($number)) {
$numbertext = get_string('questionx', 'question',
html_writer::tag('span', $number, array('class' => 'qno')));
} else if ($number == 'i') {
$numbertext = get_string('information', 'question');
}
if (!$numbertext) {
return '';
}
return html_writer::tag('h2', $numbertext, array('class' => 'no'));
}
/**
* Add an invisible heading like 'question text', 'feebdack' at the top of
* a section's contents, but only if the section has some content.
* @param string $heading the heading to add.
* @param string $content the content of the section.
* @return string HTML fragment with the heading added.
*/
protected function add_part_heading($heading, $content) {
if ($content) {
$content = html_writer::tag('h3', $heading, array('class' => 'accesshide')) . $content;
}
return $content;
}
/**
* Generate the display of the status line that gives the current state of
* the question.
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
protected function status(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) {
return html_writer::tag('div', $qa->get_state_string($options->correctness),
array('class' => 'state'));
}
/**
* Generate the display of the marks for this question.
* @param question_attempt $qa the question attempt to display.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
protected function mark_summary(question_attempt $qa, question_display_options $options) {
if (!$options->marks) {
return '';
}
if ($qa->get_max_mark() == 0) {
$summary = get_string('notgraded', 'question');
} else if ($options->marks == question_display_options::MAX_ONLY ||
is_null($qa->get_fraction())) {
$summary = get_string('markedoutofmax', 'question', $qa->format_max_mark($options->markdp));
} else {
$a = new stdClass;
$a->mark = $qa->format_mark($options->markdp);
$a->max = $qa->format_max_mark($options->markdp);
$summary = get_string('markoutofmax', 'question', $a);
}
return html_writer::tag('div', $summary, array('class' => 'grade'));
}
/**
* Render the question flag, assuming $flagsoption allows it.
*
* @param question_attempt $qa the question attempt to display.
* @param integer $flagsoption the option that says whether flags should be displayed.
*/
protected function question_flag(question_attempt $qa, $flagsoption) {
global $CFG;
switch ($flagsoption) {
case question_display_options::VISIBLE:
$flagcontent = $this->get_flag_html($qa->is_flagged());
break;
case question_display_options::EDITABLE:
$id = $qa->get_flag_field_name();
if ($qa->is_flagged()) {
$checked = 'checked="checked" ';
} else {
$checked = '';
}
$postdata = question_flags::get_postdata($qa);
// The checkbox id must be different from any element name, becuase
// of a stupid IE bug: http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/
$flagcontent = '<input type="hidden" name="' . $id . '" value="0" />' .
'<input type="checkbox" id="' . $id . 'checkbox" name="' . $id . '" value="1" ' . $checked . ' />' .
'<label id="' . $id . 'label" for="' . $id . '">' . $this->get_flag_html(
$qa->is_flagged(), $id . 'img') . '</label>' . "\n" .
print_js_call('question_flag_changer.init_flag', array($id, $postdata, $qa->get_slot()), true);
break;
default:
$flagcontent = '';
}
if ($flagcontent) {
return '<div class="questionflag">' . $flagcontent . "</div>\n";
}
}
/**
* Work out the actual img tag needed for the flag
*
* @param boolean $flagged whether the question is currently flagged.
* @param string $id an id to be added as an attribute to the img (optional).
* @return string the img tag.
*/
protected function get_flag_html($flagged, $id = '') {
global $CFG;
if ($id) {
$id = 'id="' . $id . '" ';
}
if ($flagged) {
$img = 'flagged.png';
} else {
$img = 'unflagged.png';
}
return '<img ' . $id . 'src="' . $CFG->pixpath . '/i/' . $img .
'" alt="' . get_string('flagthisquestion', 'question') . '" />';
}
/**
* Generate the display of the formulation part of the question. This is the
* area that contains the quetsion text, and the controls for students to
* input their answers. Some question types also embed feedback, for
* example ticks and crosses, in this area.
*
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param qtype_renderer $qtoutput the renderer to output the question type
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options) {
$output = '';
$output .= html_writer::empty_tag('input', array(
'type' => 'hidden',
'name' => $qa->get_control_field_name('sequencecheck'),
'value' => $qa->get_num_steps()));
$output .= $qtoutput->formulation_and_controls($qa, $options);
if ($options->clearwrong) {
$output .= $qtoutput->clear_wrong($qa);
}
$output .= html_writer::nonempty_tag('div',
$behaviouroutput->controls($qa, $options), array('class' => 'im-controls'));
return $output;
}
/**
* Generate the display of the outcome part of the question. This is the
* area that contains the various forms of feedback.
*
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param qtype_renderer $qtoutput the renderer to output the question type
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options) {
$output = '';
$output .= html_writer::nonempty_tag('div',
$qtoutput->feedback($qa, $options), array('class' => 'feedback'));
$output .= html_writer::nonempty_tag('div',
$behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback'));
return $output;
}
/**
* Generate the display of the response history part of the question. This
* is the table showing all the steps the question has been through.
*
* @param question_attempt $qa the question attempt to display.
* @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
* specific parts.
* @param qtype_renderer $qtoutput the renderer to output the question type
* specific parts.
* @param question_display_options $options controls what should and should not be displayed.
* @return HTML fragment.
*/
protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options) {
if (!$options->history) {
return '';
}
$table = new stdClass;
$table->head = array (
get_string('step', 'question'),
get_string('time'),
get_string('action', 'question'),
get_string('state', 'question'),
);
if ($options->marks >= question_display_options::MARK_AND_MAX) {
$table->head[] = get_string('marks', 'question');
}
foreach ($qa->get_full_step_iterator() as $i => $step) {
$stepno = $i + 1;
$rowclass = '';
if ($stepno == $qa->get_num_steps()) {
$rowclass = 'current';
} else if (!empty($options->questionreviewlink)) {
$stepno = link_to_popup_window($options->questionreviewlink .
'&amp;slot=' . $qa->get_slot() .
'&step=' . $i, 'reviewquestion', $stepno, 450, 650,
get_string('reviewresponse', 'quiz'), 'none', true);
}
$user = new stdClass;
$user->id = $step->get_user_id();
$row = array(
$stepno,
userdate($step->get_timecreated(), get_string('strftimedatetimeshort')),
s($qa->summarise_action($step)),
$step->get_state()->default_string(true),
);
if ($options->marks >= question_display_options::MARK_AND_MAX) {
$row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
}
$table->rowclass[] = $rowclass;
$table->data[] = $row;
}
return html_writer::tag('h3', get_string('responsehistory', 'question'),
array('class' => 'responsehistoryheader')) . html_writer::tag('div',
print_table($table, true), array('class' => 'responsehistoryheader'));
}
}

View File

@ -0,0 +1,629 @@
<?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 file contains helper classes for testing the question engine.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
/**
* Makes some protected methods of question_attempt public to facilitate testing.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_question_attempt extends question_attempt {
public function add_step($step) {#
parent::add_step($step);
}
public function set_min_fraction($fraction) {
$this->minfraction = $fraction;
}
public function set_behaviour(question_behaviour $behaviour) {
$this->behaviour = $behaviour;
}
}
/**
* This class creates questions of various types, which can then be used when
* testing.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_question_maker {
const STANDARD_OVERALL_CORRECT_FEEDBACK = 'Well done!';
const STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK = 'Parts, but only parts, of your response are correct.';
const STANDARD_OVERALL_INCORRECT_FEEDBACK = 'That is not right at all.';
/**
* Initialise the common fields of a question of any type.
*/
public static function initialise_a_question($q) {
global $USER;
$q->id = 0;
$q->category = 0;
$q->parent = 0;
$q->questiontextformat = FORMAT_HTML;
$q->defaultmark = 1;
$q->penalty = 0.3333333;
$q->length = 1;
$q->stamp = make_unique_id_code();
$q->version = make_unique_id_code();
$q->hidden = 0;
$q->timecreated = time();
$q->timemodified = time();
$q->createdby = $USER->id;
$q->modifiedby = $USER->id;
}
/**
* Makes a truefalse question with correct answer true, defaultmark 1.
* @return qtype_truefalse_question
*/
public static function make_a_truefalse_question() {
question_bank::load_question_definition_classes('truefalse');
$tf = new qtype_truefalse_question();
self::initialise_a_question($tf);
$tf->name = 'True/false question';
$tf->questiontext = 'The answer is true.';
$tf->generalfeedback = 'You should have selected true.';
$tf->penalty = 1;
$tf->qtype = question_bank::get_qtype('truefalse');
$tf->rightanswer = true;
$tf->truefeedback = 'This is the right answer.';
$tf->falsefeedback = 'This is the wrong answer.';
return $tf;
}
/**
* Makes a multichoice question with choices 'A', 'B' and 'C' shuffled. 'A'
* is correct, defaultmark 1.
* @return qtype_multichoice_single_question
*/
public static function make_a_multichoice_single_question() {
question_bank::load_question_definition_classes('multichoice');
$mc = new qtype_multichoice_single_question();
self::initialise_a_question($mc);
$mc->name = 'Multi-choice question, single response';
$mc->questiontext = 'The answer is A.';
$mc->generalfeedback = 'You should have selected A.';
$mc->qtype = question_bank::get_qtype('multichoice');
$mc->shuffleanswers = 1;
$mc->answernumbering = 'abc';
$mc->answers = array(
13 => new question_answer('A', 1, 'A is right'),
14 => new question_answer('B', -0.3333333, 'B is wrong'),
15 => new question_answer('C', -0.3333333, 'C is wrong'),
);
return $mc;
}
/**
* Makes a multichoice question with choices 'A', 'B', 'C' and 'D' shuffled.
* 'A' and 'C' is correct, defaultmark 1.
* @return qtype_multichoice_multi_question
*/
public static function make_a_multichoice_multi_question() {
question_bank::load_question_definition_classes('multichoice');
$mc = new qtype_multichoice_multi_question();
self::initialise_a_question($mc);
$mc->name = 'Multi-choice question, multiple response';
$mc->questiontext = 'The answer is A and C.';
$mc->generalfeedback = 'You should have selected A and C.';
$mc->qtype = question_bank::get_qtype('multichoice');
$mc->shuffleanswers = 1;
$mc->answernumbering = 'abc';
self::set_standard_combined_feedback_fields($mc);
$mc->answers = array(
13 => new question_answer('A', 0.5, 'A is part of the right answer'),
14 => new question_answer('B', -1, 'B is wrong'),
15 => new question_answer('C', 0.5, 'C is part of the right answer'),
16 => new question_answer('D', -1, 'D is wrong'),
);
return $mc;
}
/**
* Makes a matching question to classify 'Dog', 'Frog', 'Toad' and 'Cat' as
* 'Mammal', 'Amphibian' or 'Insect'.
* defaultmark 1. Stems are shuffled by default.
* @return qtype_match_question
*/
public static function make_a_matching_question() {
question_bank::load_question_definition_classes('match');
$match = new qtype_match_question();
self::initialise_a_question($match);
$match->name = 'Matching question';
$match->questiontext = 'Classify the animals.';
$match->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
$match->qtype = question_bank::get_qtype('match');
$match->shufflestems = 1;
self::set_standard_combined_feedback_fields($match);
$match->stems = array('', 'Dog', 'Frog', 'Toad', 'Cat');
$match->choices = array('', 'Mammal', 'Amphibian', 'Insect');
$match->right = array('', 1, 2, 2, 1);
unset($match->stems[0]);
unset($match->choices[0]);
unset($match->right[0]);
return $match;
}
/**
* Makes a shortanswer question with correct ansewer 'frog', partially
* correct answer 'toad' and defaultmark 1.
* @return qtype_shortanswer_question
*/
public static function make_a_shortanswer_question() {
question_bank::load_question_definition_classes('shortanswer');
$sa = new qtype_shortanswer_question();
self::initialise_a_question($sa);
$sa->name = 'Short answer question';
$sa->questiontext = 'Name an amphibian: __________';
$sa->generalfeedback = 'Generalfeedback: frog or toad would have been OK.';
$sa->usecase = false;
$sa->answers = array(
new question_answer('frog', 1.0, 'Frog is a very good answer.'),
new question_answer('toad', 0.8, 'Toad is an OK good answer.'),
new question_answer('*', 0.0, 'That is a bad answer.'),
);
$sa->qtype = question_bank::get_qtype('shortanswer');
return $sa;
}
/**
* Makes a numerical question with correct ansewer 3.14, and various incorrect
* answers with different feedback.
* @return qtype_numerical_question
*/
public static function make_a_numerical_question() {
question_bank::load_question_definition_classes('numerical');
$num = new qtype_numerical_question();
self::initialise_a_question($num);
$num->name = 'Pi to two d.p.';
$num->questiontext = 'What is pi to two d.p.?';
$num->generalfeedback = 'Generalfeedback: 3.14 is the right answer.';
$num->answers = array(
new qtype_numerical_answer('3.14', 1.0, 'Very good.', 0),
new qtype_numerical_answer('3.142', 0.0, 'Too accurate.', 0.005),
new qtype_numerical_answer('3.1', 0.0, 'Not accurate enough.', 0.05),
new qtype_numerical_answer('3', 0.0, 'Not accurate enough.', 0.5),
new qtype_numerical_answer('*', 0.0, 'Completely wrong.', 0),
);
$num->qtype = question_bank::get_qtype('numerical');
$num->ap = new qtype_numerical_answer_processor(array());
return $num;
}
/**
* Makes a truefalse question with correct ansewer true, defaultmark 1.
* @return qtype_essay_question
*/
public static function make_an_essay_question() {
question_bank::load_question_definition_classes('essay');
$essay = new qtype_essay_question();
self::initialise_a_question($essay);
$essay->name = 'Essay question';
$essay->questiontext = 'Write an essay.';
$essay->generalfeedback = 'I hope you wrote an interesting essay.';
$essay->penalty = 0;
$essay->qtype = question_bank::get_qtype('essay');
return $essay;
}
/**
* Makes a truefalse question with correct ansewer true, defaultmark 1.
* @return question_truefalse
*/
public static function make_a_description_question() {
question_bank::load_question_definition_classes('description');
$description = new qtype_description_question();
self::initialise_a_question($description);
$description->name = 'Description question';
$description->questiontext = 'This text tells you a bit about the next few questions in this quiz.';
$description->generalfeedback = 'This is what this seciton of the quiz should have taught you.';
$description->qtype = question_bank::get_qtype('description');
return $description;
}
/**
* Add some standard overall feedback to a question. You need to use these
* specific feedback strings for the corresponding contains_..._feedback
* methods in {@link qbehaviour_walkthrough_test_base} to works.
* @param question_definition $q the question to add the feedback to.
*/
public static function set_standard_combined_feedback_fields($q) {
$q->correctfeedback = self::STANDARD_OVERALL_CORRECT_FEEDBACK;
$q->partiallycorrectfeedback = self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK;
$q->shownumcorrect = true;
$q->incorrectfeedback = self::STANDARD_OVERALL_INCORRECT_FEEDBACK;
}
}
abstract class testing_db_record_builder {
public static function build_db_records(array $table) {
$columns = array_shift($table);
$records = array();
foreach ($table as $row) {
if (count($row) != count($columns)) {
throw new Exception("Row contains the wrong number of fields.");
}
$rec = new stdClass;
foreach ($columns as $i => $name) {
$rec->$name = $row[$i];
}
$records[] = $rec;
}
return $records;
}
}
class data_loading_method_test_base extends UnitTestCase {
public function build_db_records(array $table) {
return testing_db_record_builder::build_db_records($table);
}
}
class qbehaviour_walkthrough_test_base extends UnitTestCase {
/** @var question_display_options */
protected $displayoptions;
/** @var question_usage_by_activity */
protected $quba;
/** @var unknown_type integer */
protected $slot;
public function setUp() {
$this->displayoptions = new question_display_options();
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
}
public function tearDown() {
$this->displayoptions = null;
$this->quba = null;
}
protected function start_attempt_at_question($question, $preferredbehaviour, $maxmark = null) {
$this->quba->set_preferred_behaviour($preferredbehaviour);
$this->slot = $this->quba->add_question($question, $maxmark);
$this->quba->start_all_questions();
}
protected function process_submission($data) {
$this->quba->process_action($this->slot, $data);
}
protected function manual_grade($comment, $mark) {
$this->quba->manual_grade($this->slot, $comment, $mark);
}
protected function check_current_state($state) {
$this->assertEqual($this->quba->get_question_state($this->slot), $state, 'Questions is in the wrong state: %s.');
}
protected function check_current_mark($mark) {
if (is_null($mark)) {
$this->assertNull($this->quba->get_question_mark($this->slot));
} else {
if ($mark == 0) {
// PHP will think a null mark and a mark of 0 are equal,
// so explicity check not null in this case.
$this->assertNotNull($this->quba->get_question_mark($this->slot));
}
$this->assertWithinMargin($mark, $this->quba->get_question_mark($this->slot),
0.000001, 'Expected mark and actual mark differ: %s.');
}
}
/**
* @param $condition one or more Expectations. (users varargs).
*/
protected function check_current_output() {
$html = $this->quba->render_question($this->slot, $this->displayoptions);
foreach (func_get_args() as $condition) {
$this->assert($condition, $html);
}
}
protected function get_question_attempt() {
return $this->quba->get_question_attempt($this->slot);
}
protected function get_step_count() {
return $this->get_question_attempt()->get_num_steps();
}
protected function check_step_count($expectednumsteps) {
$this->assertEqual($expectednumsteps, $this->get_step_count());
}
protected function get_step($stepnum) {
return $this->get_question_attempt()->get_step($stepnum);
}
protected function get_contains_question_text_expectation($question) {
return new PatternExpectation('/' . preg_quote($question->questiontext) . '/');
}
protected function get_contains_general_feedback_expectation() {
return new PatternExpectation('/' . preg_quote($question->generalfeedback) . '/');
}
protected function get_does_not_contain_correctness_expectation() {
return new NoPatternExpectation('/class=\"correctness/');
}
protected function get_contains_correct_expectation() {
return new PatternExpectation('/' . preg_quote(get_string('correct', 'question')) . '/');
}
protected function get_contains_partcorrect_expectation() {
return new PatternExpectation('/' . preg_quote(get_string('partiallycorrect', 'question')) . '/');
}
protected function get_contains_incorrect_expectation() {
return new PatternExpectation('/' . preg_quote(get_string('incorrect', 'question')) . '/');
}
protected function get_contains_standard_correct_combined_feedback_expectation() {
return new PatternExpectation('/' . preg_quote(test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK) . '/');
}
protected function get_contains_standard_partiallycorrect_combined_feedback_expectation() {
return new PatternExpectation('/' . preg_quote(test_question_maker::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK) . '/');
}
protected function get_contains_standard_incorrect_combined_feedback_expectation() {
return new PatternExpectation('/' . preg_quote(test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK) . '/');
}
protected function get_does_not_contain_feedback_expectation() {
return new NoPatternExpectation('/class="feedback"/');
}
protected function get_does_not_contain_num_parts_correct() {
return new NoPatternExpectation('/class="numpartscorrect"/');
}
protected function get_contains_num_parts_correct($num) {
$a = new stdClass;
$a->num = $num;
return new PatternExpectation('/<div class="numpartscorrect">' .
preg_quote(get_string('yougotnright', 'question', $a)) . '/');
}
protected function get_does_not_contain_specific_feedback_expectation() {
return new NoPatternExpectation('/class="specificfeedback"/');
}
protected function get_contains_validation_error_expectation() {
return new ContainsTagWithAttribute('div', 'class', 'validationerror');
}
protected function get_does_not_contain_validation_error_expectation() {
return new NoPatternExpectation('/class="validationerror"/');
}
protected function get_contains_mark_summary($mark) {
$a = new stdClass;
$a->mark = format_float($mark, $this->displayoptions->markdp);
$a->max = format_float($this->quba->get_question_max_mark($this->slot),
$this->displayoptions->markdp);
return new PatternExpectation('/' .
preg_quote(get_string('markoutofmax', 'question', $a)) . '/');
}
protected function get_contains_marked_out_of_summary() {
$max = format_float($this->quba->get_question_max_mark($this->slot),
$this->displayoptions->markdp);
return new PatternExpectation('/' .
preg_quote(get_string('markedoutofmax', 'question', $max)) . '/');
}
protected function get_does_not_contain_mark_summary() {
return new NoPatternExpectation('/<div class="grade">/');
}
protected function get_contains_checkbox_expectation($baseattr, $enabled, $checked) {
$expectedattributes = $baseattr;
$forbiddenattributes = array();
$expectedattributes['type'] = 'checkbox';
if ($enabled === true) {
$forbiddenattributes['disabled'] = 'disabled';
} else if ($enabled === false) {
$expectedattributes['disabled'] = 'disabled';
}
if ($checked === true) {
$expectedattributes['checked'] = 'checked';
} else if ($checked === false) {
$forbiddenattributes['checked'] = 'checked';
}
return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
}
protected function get_contains_mc_checkbox_expectation($index, $enabled = null, $checked = null) {
return $this->get_contains_checkbox_expectation(array(
'name' => $this->quba->get_field_prefix($this->slot) . $index,
'value' => 1,
), $enabled, $checked);
}
protected function get_contains_radio_expectation($baseattr, $enabled, $checked) {
$expectedattributes = $baseattr;
$forbiddenattributes = array();
$expectedattributes['type'] = 'radio';
if ($enabled === true) {
$forbiddenattributes['disabled'] = 'disabled';
} else if ($enabled === false) {
$expectedattributes['disabled'] = 'disabled';
}
if ($checked === true) {
$expectedattributes['checked'] = 'checked';
} else if ($checked === false) {
$forbiddenattributes['checked'] = 'checked';
}
return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
}
protected function get_contains_mc_radio_expectation($index, $enabled = null, $checked = null) {
return $this->get_contains_radio_expectation(array(
'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
'value' => $index,
), $enabled, $checked);
}
protected function get_contains_hidden_expectation($name, $value = null) {
$expectedattributes = array('type' => 'hidden', 'name' => s($name));
if (!is_null($value)) {
$expectedattributes['value'] = s($value);
}
return new ContainsTagWithAttributes('input', $expectedattributes);
}
protected function get_does_not_contain_hidden_expectation($name, $value = null) {
$expectedattributes = array('type' => 'hidden', 'name' => s($name));
if (!is_null($value)) {
$expectedattributes['value'] = s($value);
}
return new DoesNotContainTagWithAttributes('input', $expectedattributes);
}
protected function get_contains_tf_true_radio_expectation($enabled = null, $checked = null) {
return $this->get_contains_radio_expectation(array(
'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
'value' => 1,
), $enabled, $checked);
}
protected function get_contains_tf_false_radio_expectation($enabled = null, $checked = null) {
return $this->get_contains_radio_expectation(array(
'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
'value' => 0,
), $enabled, $checked);
}
protected function get_contains_cbm_radio_expectation($certainty, $enabled = null, $checked = null) {
return $this->get_contains_radio_expectation(array(
'name' => $this->quba->get_field_prefix($this->slot) . '-certainty',
'value' => $certainty,
), $enabled, $checked);
}
protected function get_contains_button_expectation($name, $value = null, $enabled = null) {
$expectedattributes = array(
'type' => 'submit',
'name' => $name,
);
$forbiddenattributes = array();
if (!is_null($value)) {
$expectedattributes['value'] = $value;
}
if ($enabled === true) {
$forbiddenattributes['disabled'] = 'disabled';
} else if ($enabled === false) {
$expectedattributes['disabled'] = 'disabled';
}
return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
}
protected function get_contains_submit_button_expectation($enabled = null) {
return $this->get_contains_button_expectation(
$this->quba->get_field_prefix($this->slot) . '-submit', null, $enabled);
}
protected function get_tries_remaining_expectation($n) {
return new PatternExpectation('/' . preg_quote(get_string('triesremaining', 'qbehaviour_interactive', $n)) . '/');
}
protected function get_invalid_answer_expectation() {
return new PatternExpectation('/' . preg_quote(get_string('invalidanswer', 'question')) . '/');
}
protected function get_contains_try_again_button_expectation($enabled = null) {
$expectedattributes = array(
'type' => 'submit',
'name' => $this->quba->get_field_prefix($this->slot) . '-tryagain',
);
$forbiddenattributes = array();
if ($enabled === true) {
$forbiddenattributes['disabled'] = 'disabled';
} else if ($enabled === false) {
$expectedattributes['disabled'] = 'disabled';
}
return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
}
protected function get_does_not_contain_try_again_button_expectation() {
return new NoPatternExpectation('/name="' .
$this->quba->get_field_prefix($this->slot) . '-tryagain"/');
}
protected function get_contains_select_expectation($name, $choices,
$selected = null, $enabled = null) {
$fullname = $this->quba->get_field_prefix($this->slot) . $name;
return new ContainsSelectExpectation($fullname, $choices, $selected, $enabled);
}
protected function get_mc_right_answer_index($mc) {
$order = $mc->get_order($this->get_question_attempt());
foreach ($order as $i => $ansid) {
if ($mc->answers[$ansid]->fraction == 1) {
return $i;
}
}
$this->fail('This multiple choice question does not seem to have a right answer!');
}
protected function get_no_hint_visible_expectation() {
return new NoPatternExpectation('/class="hint"/');
}
protected function get_contains_hint_expectation($hinttext) {
// Does not currently verify hint text.
return new ContainsTagWithAttribute('div', 'class', 'hint');
}
}

View File

@ -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/>.
/**
* This file contains tests for the question_state class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
class qubaid_condition_test extends UnitTestCase {
protected function check_typical_question_attempts_query(
qubaid_condition $qubaids, $expectedsql, $expectedparams) {
$sql = "SELECT qa.id, qa.maxmark
FROM {$qubaids->from_question_attempts('qa')}
WHERE {$qubaids->where()} AND qa.slot = :slot";
$this->assertEqual($expectedsql, $sql);
$params = $qubaids->from_where_params();
$params['slot'] = 1;
$this->assertEqual($expectedparams, $params);
}
protected function check_typical_in_query(qubaid_condition $qubaids, $expectedsql, $expectedparams) {
global $CFG;
$sql = "SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid {$qubaids->usage_id_in()}";
$this->assertEqual($expectedsql, $sql);
$this->assertEqual($expectedparams, $qubaids->usage_id_in_params());
}
public function test_qubaid_list_one_join() {
global $CFG;
$qubaids = new qubaid_list(array(1));
$this->check_typical_question_attempts_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid = :qubaid0000 AND qa.slot = :slot",
array('qubaid0000' => 1, 'slot' => 1));
}
public function test_qubaid_list_several_join() {
global $CFG;
$qubaids = new qubaid_list(array(1, 3, 7));
$this->check_typical_question_attempts_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid IN (:qubaid0000,:qubaid0001,:qubaid0002) AND qa.slot = :slot",
array('qubaid0000' => 1, 'qubaid0001' => 3, 'qubaid0002' => 7, 'slot' => 1));
}
public function test_qubaid_join() {
global $CFG;
$qubaids = new qubaid_join("{$CFG->prefix}other_table ot", 'ot.usageid', 'ot.id = 1');
$this->check_typical_question_attempts_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}other_table ot
JOIN {$CFG->prefix}question_attempts qa ON qa.questionusageid = ot.usageid
WHERE ot.id = 1 AND qa.slot = :slot", array('slot' => 1));
}
public function test_qubaid_join_no_where_join() {
global $CFG;
$qubaids = new qubaid_join("{$CFG->prefix}other_table ot", 'ot.usageid');
$this->check_typical_question_attempts_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}other_table ot
JOIN {$CFG->prefix}question_attempts qa ON qa.questionusageid = ot.usageid
WHERE 1 = 1 AND qa.slot = :slot", array('slot' => 1));
}
public function test_qubaid_list_one_in() {
global $CFG;
$qubaids = new qubaid_list(array(1));
$this->check_typical_in_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid = :qubaid0000", array('qubaid0000' => 1));
}
public function test_qubaid_list_several_in() {
global $CFG;
$qubaids = new qubaid_list(array(1, 2, 3));
$this->check_typical_in_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid IN (:qubaid0000,:qubaid0001,:qubaid0002)",
array('qubaid0000' => 1, 'qubaid0001' => 2, 'qubaid0002' => 3));
}
public function test_qubaid_join_in() {
global $CFG;
$qubaids = new qubaid_join("{$CFG->prefix}other_table ot", 'ot.usageid', 'ot.id = 1');
$this->check_typical_in_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid IN (SELECT ot.usageid FROM {$CFG->prefix}other_table ot WHERE ot.id = 1)",
array());
}
public function test_qubaid_join_no_where_in() {
global $CFG;
$qubaids = new qubaid_join("{$CFG->prefix}other_table ot", 'ot.usageid');
$this->check_typical_in_query($qubaids,
"SELECT qa.id, qa.maxmark
FROM {$CFG->prefix}question_attempts qa
WHERE qa.questionusageid IN (SELECT ot.usageid FROM {$CFG->prefix}other_table ot WHERE 1 = 1)",
array());
}
}

View File

@ -0,0 +1,347 @@
<?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 file contains tests for the question_attempt class.
*
* Action methods like start, process_action and finish are assumed to be
* tested by testintegration.php.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once(dirname(__FILE__) . '/helpers.php');
class question_attempt_test extends UnitTestCase {
private $question;
private $usageid;
private $qa;
public function setUp() {
$this->question = test_question_maker::make_a_description_question();
$this->question->defaultmark = 3;
$this->usageid = 13;
$this->qa = new question_attempt($this->question, $this->usageid);
}
public function tearDown() {
$this->question = null;
$this->useageid = null;
$this->qa = null;
}
public function test_constructor_sets_maxmark() {
$qa = new question_attempt($this->question, $this->usageid);
$this->assertIdentical($this->question, $qa->get_question());
$this->assertEqual(3, $qa->get_max_mark());
}
public function test_maxmark_beats_default_mark() {
$qa = new question_attempt($this->question, $this->usageid, null, 2);
$this->assertEqual(2, $qa->get_max_mark());
}
public function test_get_set_number_in_usage() {
$this->qa->set_number_in_usage(7);
$this->assertEqual(7, $this->qa->get_slot());
}
public function test_fagged_initially_false() {
$this->assertEqual(false, $this->qa->is_flagged());
}
public function test_set_is_flagged() {
$this->qa->set_flagged(true);
$this->assertEqual(true, $this->qa->is_flagged());
}
public function test_get_qt_field_name() {
$name = $this->qa->get_qt_field_name('test');
$this->assertPattern('/^' . preg_quote($this->qa->get_field_prefix()) . '/', $name);
$this->assertPattern('/_test$/', $name);
}
public function test_get_behaviour_field_name() {
$name = $this->qa->get_behaviour_field_name('test');
$this->assertPattern('/^' . preg_quote($this->qa->get_field_prefix()) . '/', $name);
$this->assertPattern('/_-test$/', $name);
}
public function test_get_field_prefix() {
$this->qa->set_number_in_usage(7);
$name = $this->qa->get_field_prefix();
$this->assertPattern('/' . preg_quote($this->usageid) . '/', $name);
$this->assertPattern('/' . preg_quote($this->qa->get_slot()) . '/', $name);
}
public function test_get_submitted_var_not_present_var_returns_null() {
$this->assertNull(question_attempt::get_submitted_var(
'reallyunlikelyvariablename', PARAM_BOOL));
}
public function test_get_submitted_var_param_mark_not_present() {
$this->assertNull(question_attempt::get_submitted_var(
'name', question_attempt::PARAM_MARK, array()));
}
public function test_get_submitted_var_param_mark_blank() {
$this->assertIdentical('', question_attempt::get_submitted_var(
'name', question_attempt::PARAM_MARK, array('name' => '')));
}
public function test_get_submitted_var_param_mark_number() {
$this->assertIdentical(123.0, question_attempt::get_submitted_var(
'name', question_attempt::PARAM_MARK, array('name' => '123')));
}
public function test_get_submitted_var_param_mark_invalid() {
$this->assertIdentical(0.0, question_attempt::get_submitted_var(
'name', question_attempt::PARAM_MARK, array('name' => 'frog')));
}
}
/**
* These tests use a standard fixture of a question_attempt with three steps.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_attempt_with_steps_test extends UnitTestCase {
private $question;
private $qa;
public function setUp() {
$this->question = test_question_maker::make_a_description_question();
$this->qa = new testable_question_attempt($this->question, 0, null, 2);
for ($i = 0; $i < 3; $i++) {
$step = new question_attempt_step(array('i' => $i));
$this->qa->add_step($step);
}
}
public function tearDown() {
$this->qa = null;
}
public function test_get_step_before_start() {
$this->expectException();
$step = $this->qa->get_step(-1);
}
public function test_get_step_at_start() {
$step = $this->qa->get_step(0);
$this->assertEqual(0, $step->get_qt_var('i'));
}
public function test_get_step_at_end() {
$step = $this->qa->get_step(2);
$this->assertEqual(2, $step->get_qt_var('i'));
}
public function test_get_step_past_end() {
$this->expectException();
$step = $this->qa->get_step(3);
}
public function test_get_num_steps() {
$this->assertEqual(3, $this->qa->get_num_steps());
}
public function test_get_last_step() {
$step = $this->qa->get_last_step();
$this->assertEqual(2, $step->get_qt_var('i'));
}
public function test_get_last_qt_var_there1() {
$this->assertEqual(2, $this->qa->get_last_qt_var('i'));
}
public function test_get_last_qt_var_there2() {
$this->qa->get_step(0)->set_qt_var('_x', 'a value');
$this->assertEqual('a value', $this->qa->get_last_qt_var('_x'));
}
public function test_get_last_qt_var_missing() {
$this->assertNull($this->qa->get_last_qt_var('notthere'));
}
public function test_get_last_qt_var_missing_default() {
$this->assertEqual('default', $this->qa->get_last_qt_var('notthere', 'default'));
}
public function test_get_last_behaviour_var_missing() {
$this->assertNull($this->qa->get_last_qt_var('notthere'));
}
public function test_get_last_behaviour_var_there() {
$this->qa->get_step(1)->set_behaviour_var('_x', 'a value');
$this->assertEqual('a value', '' . $this->qa->get_last_behaviour_var('_x'));
}
public function test_get_state_gets_state_of_last() {
$this->qa->get_step(2)->set_state(question_state::$gradedright);
$this->qa->get_step(1)->set_state(question_state::$gradedwrong);
$this->assertEqual(question_state::$gradedright, $this->qa->get_state());
}
public function test_get_mark_gets_mark_of_last() {
$this->assertEqual(2, $this->qa->get_max_mark());
$this->qa->get_step(2)->set_fraction(0.5);
$this->qa->get_step(1)->set_fraction(0.1);
$this->assertEqual(1, $this->qa->get_mark());
}
public function test_get_fraction_gets_fraction_of_last() {
$this->qa->get_step(2)->set_fraction(0.5);
$this->qa->get_step(1)->set_fraction(0.1);
$this->assertEqual(0.5, $this->qa->get_fraction());
}
public function test_get_fraction_returns_null_if_none() {
$this->assertNull($this->qa->get_fraction());
}
public function test_format_mark() {
$this->qa->get_step(2)->set_fraction(0.5);
$this->assertEqual('1.00', $this->qa->format_mark(2));
}
public function test_format_max_mark() {
$this->assertEqual('2.0000000', $this->qa->format_max_mark(7));
}
public function test_get_min_fraction() {
$this->qa->set_min_fraction(-1);
$this->assertEqual(-1, $this->qa->get_min_fraction(0));
}
public function test_cannot_get_min_fraction_before_start() {
$qa = new question_attempt($this->question, null);
$this->expectException();
$qa->get_min_fraction();
}
}
class question_attempt_db_test extends data_loading_method_test_base {
public function test_load() {
$records = testing_db_record_builder::build_db_records(array(
array('id', 'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'maxmark', 'minfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
array(2, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'complete', null, 1256233705, 1, 'answer', '1'),
array(3, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 1, '', '', '', 1256233790, 3, 2, 'complete', null, 1256233710, 1, 'answer', '0'),
array(4, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 4, 3, 'complete', null, 1256233715, 1, 'answer', '1'),
array(5, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 4, 'gradedright', 1.0000000, 1256233720, 1, '-finish', '1'),
array(6, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-comment', 'Not good enough!'),
array(7, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-mark', '1'),
array(8, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 6, 5, 'mangrpartial', 0.5000000, 1256233790, 1, '-maxmark', '2'),
));
$question = test_question_maker::make_a_truefalse_question();
$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->assertEqual($question->questiontext, $qa->get_question()->questiontext);
$this->assertEqual(6, $qa->get_num_steps());
$step = $qa->get_step(0);
$this->assertEqual(question_state::$todo, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233700, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array(), $step->get_all_data());
$step = $qa->get_step(1);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233705, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('answer' => '1'), $step->get_all_data());
$step = $qa->get_step(2);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233710, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('answer' => '0'), $step->get_all_data());
$step = $qa->get_step(3);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233715, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('answer' => '1'), $step->get_all_data());
$step = $qa->get_step(4);
$this->assertEqual(question_state::$gradedright, $step->get_state());
$this->assertEqual(1, $step->get_fraction());
$this->assertEqual(1256233720, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('-finish' => '1'), $step->get_all_data());
$step = $qa->get_step(5);
$this->assertEqual(question_state::$mangrpartial, $step->get_state());
$this->assertEqual(0.5, $step->get_fraction());
$this->assertEqual(1256233790, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array('-comment' => 'Not good enough!', '-mark' => '1', '-maxmark' => '2'),
$step->get_all_data());
}
public function test_load_missing_question() {
$records = testing_db_record_builder::build_db_records(array(
array('id', 'questionattemptid', 'questionusageid', 'slot',
'behaviour', 'questionid', 'maxmark', 'minfraction', 'flagged',
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
'attemptstepid', 'sequencenumber', 'state', 'fraction',
'timecreated', 'userid', 'name', 'value'),
array(1, 1, 1, 1, 'deferredfeedback', -1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
));
question_bank::start_unit_test();
$qa = question_attempt::load_from_records($records, 1, new question_usage_null_observer(), 'deferredfeedback');
question_bank::end_unit_test();
$missingq = question_bank::get_qtype('missingtype')->make_deleted_instance(-1, 2);
$this->assertEqual($missingq, $qa->get_question());
$this->assertEqual(1, $qa->get_num_steps());
$step = $qa->get_step(0);
$this->assertEqual(question_state::$todo, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256233700, $step->get_timecreated());
$this->assertEqual(1, $step->get_user_id());
$this->assertEqual(array(), $step->get_all_data());
}
}

View File

@ -0,0 +1,110 @@
<?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 file contains tests for the question_attempt_iterator class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once(dirname(__FILE__) . '/helpers.php');
class question_attempt_iterator_test extends UnitTestCase {
private $quba;
private $qas = array();
private $iterator;
public function setUp() {
$this->quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
$this->quba->set_preferred_behaviour('deferredfeedback');
$slot = $this->quba->add_question(test_question_maker::make_a_description_question());
$this->qas[$slot] = $this->quba->get_question_attempt($slot);
$slot = $this->quba->add_question(test_question_maker::make_a_description_question());
$this->qas[$slot] = $this->quba->get_question_attempt($slot);
$this->iterator = $this->quba->get_attempt_iterator();
}
public function tearDown() {
$this->quba = null;
$this->iterator = null;
}
public function test_foreach_loop() {
$i = 1;
foreach ($this->iterator as $key => $qa) {
$this->assertEqual($i, $key);
$this->assertIdentical($this->qas[$i], $qa);
$i++;
}
$this->assertEqual(3, $i);
}
public function test_offsetExists_before_start() {
$this->assertFalse(isset($this->iterator[0]));
}
public function test_offsetExists_at_start() {
$this->assertTrue(isset($this->iterator[1]));
}
public function test_offsetExists_at_endt() {
$this->assertTrue(isset($this->iterator[2]));
}
public function test_offsetExists_past_end() {
$this->assertFalse(isset($this->iterator[3]));
}
public function test_offsetGet_before_start() {
$this->expectException();
$step = $this->iterator[0];
}
public function test_offsetGet_at_start() {
$this->assertIdentical($this->qas[1], $this->iterator[1]);
}
public function test_offsetGet_at_end() {
$this->assertIdentical($this->qas[2], $this->iterator[2]);
}
public function test_offsetGet_past_end() {
$this->expectException();
$step = $this->iterator[3];
}
public function test_cannot_set() {
$this->expectException();
$this->iterator[0] = null;
}
public function test_cannot_unset() {
$this->expectException();
unset($this->iterator[2]);
}
}

View File

@ -0,0 +1,170 @@
<?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 file contains tests for the question_attempt_step class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once(dirname(__FILE__) . '/helpers.php');
class question_attempt_step_test extends UnitTestCase {
public function test_initial_state_unprocessed() {
$step = new question_attempt_step();
$this->assertEqual(question_state::$unprocessed, $step->get_state());
}
public function test_get_set_state() {
$step = new question_attempt_step();
$step->set_state(question_state::$gradedright);
$this->assertEqual(question_state::$gradedright, $step->get_state());
}
public function test_initial_fraction_null() {
$step = new question_attempt_step();
$this->assertNull($step->get_fraction());
}
public function test_get_set_fraction() {
$step = new question_attempt_step();
$step->set_fraction(0.5);
$this->assertEqual(0.5, $step->get_fraction());
}
public function test_has_var() {
$step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
$this->assertTrue($step->has_qt_var('x'));
$this->assertTrue($step->has_behaviour_var('y'));
$this->assertFalse($step->has_qt_var('y'));
$this->assertFalse($step->has_behaviour_var('x'));
}
public function test_get_var() {
$step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
$this->assertEqual('1', $step->get_qt_var('x'));
$this->assertEqual('frog', $step->get_behaviour_var('y'));
$this->assertNull($step->get_qt_var('y'));
}
public function test_set_var() {
$step = new question_attempt_step();
$step->set_qt_var('_x', 1);
$step->set_behaviour_var('_x', 2);
$this->assertEqual('1', $step->get_qt_var('_x'));
$this->assertEqual('2', $step->get_behaviour_var('_x'));
}
public function test_cannot_set_qt_var_without_underscore() {
$step = new question_attempt_step();
$this->expectException();
$step->set_qt_var('x', 1);
}
public function test_cannot_set_behaviour_var_without_underscore() {
$step = new question_attempt_step();
$this->expectException();
$step->set_behaviour_var('x', 1);
}
public function test_get_data() {
$step = new question_attempt_step(array('x' => 1, '-y' => 'frog', ':flagged' => 1));
$this->assertEqual(array('x' => '1'), $step->get_qt_data());
$this->assertEqual(array('y' => 'frog'), $step->get_behaviour_data());
$this->assertEqual(array('x' => 1, '-y' => 'frog', ':flagged' => 1), $step->get_all_data());
}
public function test_get_submitted_data() {
$step = new question_attempt_step(array('x' => 1, '-y' => 'frog'));
$step->set_qt_var('_x', 1);
$step->set_behaviour_var('_x', 2);
$this->assertEqual(array('x' => 1, '-y' => 'frog'), $step->get_submitted_data());
}
public function test_constructor_default_params() {
global $USER;
$step = new question_attempt_step();
$this->assertWithinMargin(time(), $step->get_timecreated(), 5);
$this->assertEqual($USER->id, $step->get_user_id());
$this->assertEqual(array(), $step->get_qt_data());
$this->assertEqual(array(), $step->get_behaviour_data());
}
public function test_constructor_given_params() {
global $USER;
$step = new question_attempt_step(array(), 123, 5);
$this->assertEqual(123, $step->get_timecreated());
$this->assertEqual(5, $step->get_user_id());
$this->assertEqual(array(), $step->get_qt_data());
$this->assertEqual(array(), $step->get_behaviour_data());
}
}
class question_attempt_step_db_test extends data_loading_method_test_base {
public function test_load_with_data() {
$records = $this->build_db_records(array(
array('id', 'attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
array( 1, 1, 1, 0, 'todo', null, 1256228502, 13, null, null),
array( 2, 2, 1, 1, 'complete', null, 1256228505, 13, 'x', 'a'),
array( 3, 2, 1, 1, 'complete', null, 1256228505, 13, '_y', '_b'),
array( 4, 2, 1, 1, 'complete', null, 1256228505, 13, '-z', '!c'),
array( 5, 2, 1, 1, 'complete', null, 1256228505, 13, '-_t', '!_d'),
array( 6, 3, 1, 2, 'gradedright', 1.0, 1256228515, 13, '-finish', '1'),
));
$step = question_attempt_step::load_from_records($records, 2);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256228505, $step->get_timecreated());
$this->assertEqual(13, $step->get_user_id());
$this->assertEqual(array('x' => 'a', '_y' => '_b', '-z' => '!c', '-_t' => '!_d'), $step->get_all_data());
}
public function test_load_without_data() {
$records = $this->build_db_records(array(
array('id', 'attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
array( 2, 2, 1, 1, 'complete', null, 1256228505, 13, null, null),
));
$step = question_attempt_step::load_from_records($records, 2);
$this->assertEqual(question_state::$complete, $step->get_state());
$this->assertNull($step->get_fraction());
$this->assertEqual(1256228505, $step->get_timecreated());
$this->assertEqual(13, $step->get_user_id());
$this->assertEqual(array(), $step->get_all_data());
}
public function test_load_dont_be_too_greedy() {
$records = $this->build_db_records(array(
array('id', 'attemptstepid', 'questionattemptid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'),
array( 1, 1, 1, 0, 'todo', null, 1256228502, 13, 'x', 'right'),
array( 2, 2, 2, 0, 'complete', null, 1256228505, 13, 'x', 'wrong'),
));
$step = question_attempt_step::load_from_records($records, 1);
$this->assertEqual(array('x' => 'right'), $step->get_all_data());
}
}

View File

@ -0,0 +1,129 @@
<?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 file contains tests for the question_attempt_step_iterator class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once(dirname(__FILE__) . '/helpers.php');
class question_attempt_step_iterator_test extends UnitTestCase {
private $qa;
private $iterator;
public function setUp() {
$question = test_question_maker::make_a_description_question();
$this->qa = new testable_question_attempt($question, 0);
for ($i = 0; $i < 3; $i++) {
$step = new question_attempt_step(array('i' => $i));
$this->qa->add_step($step);
}
$this->iterator = $this->qa->get_step_iterator();
}
public function tearDown() {
$this->qa = null;
$this->iterator = null;
}
public function test_foreach_loop() {
$i = 0;
foreach ($this->iterator as $key => $step) {
$this->assertEqual($i, $key);
$this->assertEqual($i, $step->get_qt_var('i'));
$i++;
}
}
public function test_foreach_loop_add_step_during() {
$i = 0;
foreach ($this->iterator as $key => $step) {
$this->assertEqual($i, $key);
$this->assertEqual($i, $step->get_qt_var('i'));
$i++;
if ($i == 2) {
$step = new question_attempt_step(array('i' => 3));
$this->qa->add_step($step);
}
}
$this->assertEqual(4, $i);
}
public function test_reverse_foreach_loop() {
$i = 2;
foreach ($this->qa->get_reverse_step_iterator() as $key => $step) {
$this->assertEqual($i, $key);
$this->assertEqual($i, $step->get_qt_var('i'));
$i--;
}
}
public function test_offsetExists_before_start() {
$this->assertFalse(isset($this->iterator[-1]));
}
public function test_offsetExists_at_start() {
$this->assertTrue(isset($this->iterator[0]));
}
public function test_offsetExists_at_endt() {
$this->assertTrue(isset($this->iterator[2]));
}
public function test_offsetExists_past_end() {
$this->assertFalse(isset($this->iterator[3]));
}
public function test_offsetGet_before_start() {
$this->expectException();
$step = $this->iterator[-1];
}
public function test_offsetGet_at_start() {
$step = $this->iterator[0];
$this->assertEqual(0, $step->get_qt_var('i'));
}
public function test_offsetGet_at_end() {
$step = $this->iterator[2];
$this->assertEqual(2, $step->get_qt_var('i'));
}
public function test_offsetGet_past_end() {
$this->expectException();
$step = $this->iterator[3];
}
public function test_cannot_set() {
$this->expectException();
$this->iterator[0] = null;
}
public function test_cannot_unset() {
$this->expectException();
unset($this->iterator[2]);
}
}

View File

@ -0,0 +1,40 @@
<?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 file contains tests for the question_cbm class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
class question_cbm_test extends UnitTestCase {
public function test_adjust_fraction() {
$this->assertWithinMargin(0, question_cbm::adjust_fraction(0, question_cbm::LOW), 0.0000001);
$this->assertWithinMargin(-2/3, question_cbm::adjust_fraction(0, question_cbm::MED), 0.0000001);
$this->assertWithinMargin(-2, question_cbm::adjust_fraction(0, question_cbm::HIGH), 0.0000001);
$this->assertWithinMargin(1/3, question_cbm::adjust_fraction(1, question_cbm::LOW), 0.0000001);
$this->assertWithinMargin(2/3, question_cbm::adjust_fraction(1, question_cbm::MED), 0.0000001);
$this->assertWithinMargin(1, question_cbm::adjust_fraction(1, question_cbm::HIGH), 0.0000001);
}
}

View File

@ -0,0 +1,94 @@
<?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 file contains tests for the question_engine class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
class question_engine_test extends UnitTestCase {
public function setUp() {
}
public function tearDown() {
}
public function test_load_behaviour_class() {
// Exercise SUT
question_engine::load_behaviour_class('deferredfeedback');
// Verify
$this->assertTrue(class_exists('qbehaviour_deferredfeedback'));
}
public function test_load_behaviour_class_missing() {
// Set expectation.
$this->expectException();
// Exercise SUT
question_engine::load_behaviour_class('nonexistantbehaviour');
}
public function test_get_behaviour_unused_display_options() {
$this->assertEqual(array(), question_engine::get_behaviour_unused_display_options('interactive'));
$this->assertEqual(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
question_engine::get_behaviour_unused_display_options('deferredfeedback'));
$this->assertEqual(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
question_engine::get_behaviour_unused_display_options('deferredcbm'));
$this->assertEqual(array('correctness', 'marks', 'specificfeedback', 'generalfeedback', 'rightanswer'),
question_engine::get_behaviour_unused_display_options('manualgraded'));
}
public function test_sort_behaviours() {
$in = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
$out = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
$this->assertIdentical($out, question_engine::sort_behaviours($in, '', '', ''));
$this->assertIdentical($out, question_engine::sort_behaviours($in, '', 'b4', 'b4'));
$out = array('b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6');
$this->assertIdentical($out, question_engine::sort_behaviours($in, '', 'b1,b2,b3,b4', 'b4'));
$out = array('b6' => 'Behave 6', 'b1' => 'Behave 1', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6, b1, b4', 'b1, b2, b3, b4, b5', 'b4'));
$out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6, b5, b4', 'b1, b2, b3', 'b4'));
$out = array('b1' => 'Behave 1', 'b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b1, b6, b5', 'b1, b2, b3, b4, b5', 'b4'));
$out = array('b2' => 'Behave 2', 'b4' => 'Behave 4', 'b6' => 'Behave 6');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b2, b4, b6', 'b1, b3, b5', 'b2'));
// Ignore unknown input in the order argument.
$this->assertIdentical($in, question_engine::sort_behaviours($in, 'unknown', '', ''));
// Ignore unknown input in the disabled argument.
$this->assertIdentical($in, question_engine::sort_behaviours($in, '', 'unknown', ''));
}
}

View File

@ -0,0 +1,154 @@
<?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 file contains tests for the question_state class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once($CFG->libdir . '/questionlib.php');
class question_state_test extends UnitTestCase {
public function test_is_active() {
$this->assertFalse(question_state::$notstarted->is_active());
$this->assertFalse(question_state::$unprocessed->is_active());
$this->assertTrue(question_state::$todo->is_active());
$this->assertTrue(question_state::$invalid->is_active());
$this->assertTrue(question_state::$complete->is_active());
$this->assertFalse(question_state::$needsgrading->is_active());
$this->assertFalse(question_state::$finished->is_active());
$this->assertFalse(question_state::$gaveup->is_active());
$this->assertFalse(question_state::$gradedwrong->is_active());
$this->assertFalse(question_state::$gradedpartial->is_active());
$this->assertFalse(question_state::$gradedright->is_active());
$this->assertFalse(question_state::$manfinished->is_active());
$this->assertFalse(question_state::$mangaveup->is_active());
$this->assertFalse(question_state::$mangrwrong->is_active());
$this->assertFalse(question_state::$mangrpartial->is_active());
$this->assertFalse(question_state::$mangrright->is_active());
}
public function test_is_finished() {
$this->assertFalse(question_state::$notstarted->is_finished());
$this->assertFalse(question_state::$unprocessed->is_finished());
$this->assertFalse(question_state::$todo->is_finished());
$this->assertFalse(question_state::$invalid->is_finished());
$this->assertFalse(question_state::$complete->is_finished());
$this->assertTrue(question_state::$needsgrading->is_finished());
$this->assertTrue(question_state::$finished->is_finished());
$this->assertTrue(question_state::$gaveup->is_finished());
$this->assertTrue(question_state::$gradedwrong->is_finished());
$this->assertTrue(question_state::$gradedpartial->is_finished());
$this->assertTrue(question_state::$gradedright->is_finished());
$this->assertTrue(question_state::$manfinished->is_finished());
$this->assertTrue(question_state::$mangaveup->is_finished());
$this->assertTrue(question_state::$mangrwrong->is_finished());
$this->assertTrue(question_state::$mangrpartial->is_finished());
$this->assertTrue(question_state::$mangrright->is_finished());
}
public function test_is_graded() {
$this->assertFalse(question_state::$notstarted->is_graded());
$this->assertFalse(question_state::$unprocessed->is_graded());
$this->assertFalse(question_state::$todo->is_graded());
$this->assertFalse(question_state::$invalid->is_graded());
$this->assertFalse(question_state::$complete->is_graded());
$this->assertFalse(question_state::$needsgrading->is_graded());
$this->assertFalse(question_state::$finished->is_graded());
$this->assertFalse(question_state::$gaveup->is_graded());
$this->assertTrue(question_state::$gradedwrong->is_graded());
$this->assertTrue(question_state::$gradedpartial->is_graded());
$this->assertTrue(question_state::$gradedright->is_graded());
$this->assertFalse(question_state::$manfinished->is_graded());
$this->assertFalse(question_state::$mangaveup->is_graded());
$this->assertTrue(question_state::$mangrwrong->is_graded());
$this->assertTrue(question_state::$mangrpartial->is_graded());
$this->assertTrue(question_state::$mangrright->is_graded());
}
public function test_is_commented() {
$this->assertFalse(question_state::$notstarted->is_commented());
$this->assertFalse(question_state::$unprocessed->is_commented());
$this->assertFalse(question_state::$todo->is_commented());
$this->assertFalse(question_state::$invalid->is_commented());
$this->assertFalse(question_state::$complete->is_commented());
$this->assertFalse(question_state::$needsgrading->is_commented());
$this->assertFalse(question_state::$finished->is_commented());
$this->assertFalse(question_state::$gaveup->is_commented());
$this->assertFalse(question_state::$gradedwrong->is_commented());
$this->assertFalse(question_state::$gradedpartial->is_commented());
$this->assertFalse(question_state::$gradedright->is_commented());
$this->assertTrue(question_state::$manfinished->is_commented());
$this->assertTrue(question_state::$mangaveup->is_commented());
$this->assertTrue(question_state::$mangrwrong->is_commented());
$this->assertTrue(question_state::$mangrpartial->is_commented());
$this->assertTrue(question_state::$mangrright->is_commented());
}
public function test_graded_state_for_fraction() {
$this->assertEqual(question_state::$gradedwrong, question_state::graded_state_for_fraction(-1));
$this->assertEqual(question_state::$gradedwrong, question_state::graded_state_for_fraction(0));
$this->assertEqual(question_state::$gradedpartial, question_state::graded_state_for_fraction(0.000001));
$this->assertEqual(question_state::$gradedpartial, question_state::graded_state_for_fraction(0.999999));
$this->assertEqual(question_state::$gradedright, question_state::graded_state_for_fraction(1));
}
public function test_manually_graded_state_for_other_state() {
$this->assertEqual(question_state::$manfinished,
question_state::$finished->corresponding_commented_state(null));
$this->assertEqual(question_state::$mangaveup,
question_state::$gaveup->corresponding_commented_state(null));
$this->assertEqual(question_state::$manfinished,
question_state::$manfinished->corresponding_commented_state(null));
$this->assertEqual(question_state::$mangaveup,
question_state::$mangaveup->corresponding_commented_state(null));
$this->assertEqual(question_state::$needsgrading,
question_state::$mangrright->corresponding_commented_state(null));
$this->assertEqual(question_state::$needsgrading,
question_state::$mangrright->corresponding_commented_state(null));
$this->assertEqual(question_state::$mangrwrong,
question_state::$gaveup->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$needsgrading->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$gradedwrong->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$gradedpartial->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$gradedright->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$mangrright->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$mangrpartial->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrwrong,
question_state::$mangrright->corresponding_commented_state(0));
$this->assertEqual(question_state::$mangrpartial,
question_state::$gradedpartial->corresponding_commented_state(0.5));
$this->assertEqual(question_state::$mangrright,
question_state::$gradedpartial->corresponding_commented_state(1));
}
}

View File

@ -0,0 +1,155 @@
<?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 file contains tests for the question_usage_by_activity class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
require_once(dirname(__FILE__) . '/helpers.php');
class question_usage_by_activity_test extends UnitTestCase {
public function test_set_get_preferred_model() {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
// Exercise SUT and verify.
$quba->set_preferred_behaviour('deferredfeedback');
$this->assertEqual('deferredfeedback', $quba->get_preferred_behaviour());
}
public function test_set_get_id() {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
// Exercise SUT and verify
$quba->set_id_from_database(123);
$this->assertEqual(123, $quba->get_id());
}
public function test_fake_id() {
// Set up
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
// Exercise SUT and verify
$this->assertTrue($quba->get_id());
}
public function test_create_usage_and_add_question() {
// Exercise SUT
$context = get_context_instance(CONTEXT_SYSTEM);
$quba = question_engine::make_questions_usage_by_activity('unit_test', $context);
$quba->set_preferred_behaviour('deferredfeedback');
$tf = test_question_maker::make_a_truefalse_question();
$slot = $quba->add_question($tf);
// Verify.
$this->assertEqual($slot, 1);
$this->assertEqual('unit_test', $quba->get_owning_component());
$this->assertIdentical($context, $quba->get_owning_context());
$this->assertEqual($quba->question_count(), 1);
$this->assertEqual($quba->get_question_state($slot), question_state::$notstarted);
}
public function test_get_question() {
// Set up.
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
$quba->set_preferred_behaviour('deferredfeedback');
$tf = test_question_maker::make_a_truefalse_question();
$slot = $quba->add_question($tf);
// Exercise SUT and verify.
$this->assertIdentical($tf, $quba->get_question($slot));
$this->expectException();
$quba->get_question($slot + 1);
}
public function test_extract_responses() {
// Start a deferred feedback attempt with CBM and add the question to it.
$tf = test_question_maker::make_a_truefalse_question();
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
$quba->set_preferred_behaviour('deferredcbm');
$slot = $quba->add_question($tf);
$quba->start_all_questions();
// Prepare data to be submitted
$prefix = $quba->get_field_prefix($slot);
$answername = $prefix . 'answer';
$certaintyname = $prefix . '-certainty';
$getdata = array(
$answername => 1,
$certaintyname => 3,
'irrelevant' => 'should be ignored',
);
// Exercise SUT
$submitteddata = $quba->extract_responses($slot, $getdata);
// Verify.
$this->assertEqual(array('answer' => 1, '-certainty' => 3), $submitteddata);
}
public function test_access_out_of_sequence_throws_exception() {
// Start a deferred feedback attempt with CBM and add the question to it.
$tf = test_question_maker::make_a_truefalse_question();
$quba = question_engine::make_questions_usage_by_activity('unit_test',
get_context_instance(CONTEXT_SYSTEM));
$quba->set_preferred_behaviour('deferredcbm');
$slot = $quba->add_question($tf);
$quba->start_all_questions();
// Prepare data to be submitted
$prefix = $quba->get_field_prefix($slot);
$answername = $prefix . 'answer';
$certaintyname = $prefix . '-certainty';
$postdata = array(
$answername => 1,
$certaintyname => 3,
$prefix . ':sequencecheck' => 1,
'irrelevant' => 'should be ignored',
);
// Exercise SUT - no exception yet.
$quba->process_all_actions($slot, $postdata);
$postdata = array(
$answername => 1,
$certaintyname => 3,
$prefix . ':sequencecheck' => 3,
'irrelevant' => 'should be ignored',
);
// Exercise SUT - now it should fail.
$this->expectException('question_out_of_sequence_exception');
$quba->process_all_actions($slot, $postdata);
}
}

View File

@ -0,0 +1,186 @@
<?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 file contains tests for the {@link question_utils} class.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../lib.php');
class question_utils_test extends UnitTestCase {
public function test_arrays_have_same_keys_and_values() {
$this->assertTrue(question_utils::arrays_have_same_keys_and_values(
array(),
array()));
$this->assertTrue(question_utils::arrays_have_same_keys_and_values(
array('key' => 1),
array('key' => '1')));
$this->assertFalse(question_utils::arrays_have_same_keys_and_values(
array(),
array('key' => 1)));
$this->assertFalse(question_utils::arrays_have_same_keys_and_values(
array('key' => 2),
array('key' => 1)));
$this->assertFalse(question_utils::arrays_have_same_keys_and_values(
array('key' => 1),
array('otherkey' => 1)));
$this->assertFalse(question_utils::arrays_have_same_keys_and_values(
array('sub0' => '2', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1'),
array('sub0' => '1', 'sub1' => '2', 'sub2' => '3', 'sub3' => '1')));
}
public function test_arrays_same_at_key() {
$this->assertTrue(question_utils::arrays_same_at_key(
array(),
array(),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key(
array(),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key(
array('key' => 1),
array(),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key(
array('key' => 1),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key(
array('key' => 1),
array('key' => 2),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key(
array('key' => 1),
array('key' => '1'),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key(
array('key' => 0),
array('key' => ''),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key(
array(),
array('key' => ''),
'key'));
}
public function test_arrays_same_at_key_missing_is_blank() {
$this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
array(),
array(),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
array(),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
array('key' => 1),
array(),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
array('key' => 1),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
array('key' => 1),
array('key' => 2),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
array('key' => 1),
array('key' => '1'),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_missing_is_blank(
array('key' => '0'),
array('key' => ''),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_missing_is_blank(
array(),
array('key' => ''),
'key'));
}
public function test_arrays_same_at_key_integer() {
$this->assertTrue(question_utils::arrays_same_at_key_integer(
array(),
array(),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_integer(
array(),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_integer(
array('key' => 1),
array(),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_integer(
array('key' => 1),
array('key' => 1),
'key'));
$this->assertFalse(question_utils::arrays_same_at_key_integer(
array('key' => 1),
array('key' => 2),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_integer(
array('key' => 1),
array('key' => '1'),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_integer(
array('key' => '0'),
array('key' => ''),
'key'));
$this->assertTrue(question_utils::arrays_same_at_key_integer(
array(),
array('key' => 0),
'key'));
}
public function test_int_to_roman() {
$this->assertIdentical('i', question_utils::int_to_roman(1));
$this->assertIdentical('iv', question_utils::int_to_roman(4));
$this->assertIdentical('v', question_utils::int_to_roman(5));
$this->assertIdentical('vi', question_utils::int_to_roman(6));
$this->assertIdentical('ix', question_utils::int_to_roman(9));
$this->assertIdentical('xi', question_utils::int_to_roman(11));
$this->assertIdentical('xlviii', question_utils::int_to_roman(48));
$this->assertIdentical('lxxxvii', question_utils::int_to_roman(87));
$this->assertIdentical('c', question_utils::int_to_roman(100));
$this->assertIdentical('mccxxxiv', question_utils::int_to_roman(1234));
$this->assertIdentical('mmmcmxcix', question_utils::int_to_roman(3999));
}
public function test_int_to_roman_too_small() {
$this->expectException();
question_utils::int_to_roman(0);
}
public function test_int_to_roman_too_big() {
$this->expectException();
question_utils::int_to_roman(4000);
}
public function test_int_to_roman_not_int() {
$this->expectException();
question_utils::int_to_roman(1.5);
}
}

460
question/engine/states.php Normal file
View File

@ -0,0 +1,460 @@
<?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 defines the states a question can be in.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* An enumeration representing the states a question can be in after a
* {@link question_attempt_step}.
*
* There are also some useful methods for testing and manipulating states.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_state {
/**#@+
* Specific question_state instances.
*/
public static $notstarted;
public static $unprocessed;
public static $todo;
public static $invalid;
public static $complete;
public static $needsgrading;
public static $finished;
public static $gaveup;
public static $gradedwrong;
public static $gradedpartial;
public static $gradedright;
public static $manfinished;
public static $mangaveup;
public static $mangrwrong;
public static $mangrpartial;
public static $mangrright;
/**#@+-*/
protected function __construct() {
}
public static function init() {
$us = new ReflectionClass('question_state');
foreach ($us->getStaticProperties() as $name => $notused) {
$class = 'question_state_' . $name;
$states[$name] = new $class();
self::$$name = $states[$name];
}
}
/**
* Get all the states in an array.
* @return of question_state objects.
*/
public static function get_all() {
$states = array();
$us = new ReflectionClass('question_state');
foreach ($us->getStaticProperties() as $name => $notused) {
$states[] = self::$$name;
}
return $states;
}
/**
* Get all the states in an array.
* @param string $summarystate one of the four summary states
* inprogress, needsgrading, manuallygraded or autograded.
* @return arrau of the corresponding states.
*/
public static function get_all_for_summary_state($summarystate) {
$states = array();
foreach (self::get_all() as $state) {
if ($state->get_summary_state() == $summarystate) {
$states[] = $state;
}
}
if (empty($states)) {
throw new Exception('unknown summary state ' . $summarystate);
}
return $states;
}
/**
* @return string convert this state to a string.
*/
public function __toString() {
return substr(get_class($this), 15);
}
/**
* @param string $name a state name.
* @return question_state the state with that name.
*/
public static function get($name) {
return self::$$name;
}
/**
* Is this state one of the ones that mean the question attempt is in progress?
* That is, started, but no finished.
* @return boolean
*/
public function is_active() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt is finished?
* That is, no further interaction possible, apart from manual grading.
* @return boolean
*/
public function is_finished() {
return true;
}
/**
* Is this state one of the ones that mean the question attempt has been graded?
* @return boolean
*/
public function is_graded() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt has been graded?
* @return boolean
*/
public function is_correct() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt has been graded?
* @return boolean
*/
public function is_partially_correct() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt has been graded?
* @return boolean
*/
public function is_incorrect() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt has been graded?
* @return boolean
*/
public function is_gave_up() {
return false;
}
/**
* Is this state one of the ones that mean the question attempt has had a manual comment added?
* @return boolean
*/
public function is_commented() {
return false;
}
/**
* Each state can be categorised into one of four categories:
* inprogress, needsgrading, manuallygraded or autograded.
* @return string which category this state falls into.
*/
public function get_summary_state() {
if (!$this->is_finished()) {
return 'inprogress';
} else if ($this == self::$needsgrading) {
return 'needsgrading';
} else if ($this->is_commented()) {
return 'manuallygraded';
} else {
return 'autograded';
}
}
/**
* Return the appropriate graded state based on a fraction. That is 0 or less
* is $graded_incorrect, 1 is $graded_correct, otherwise it is $graded_partcorrect.
* Appropriate allowance is made for rounding float values.
*
* @param number $fraction the grade, on the fraction scale.
* @return integer one of the state constants.
*/
public static function graded_state_for_fraction($fraction) {
if ($fraction < 0.000001) {
return self::$gradedwrong;
} else if ($fraction > 0.999999) {
return self::$gradedright;
} else {
return self::$gradedpartial;
}
}
/**
* Return the appropriate manually graded state based on a fraction. That is 0 or less
* is $manually_graded_incorrect, 1 is $manually_graded_correct, otherwise it is
* $manually_graded_partcorrect. Appropriate allowance is made for rounding float values.
*
* @param number $fraction the grade, on the fraction scale.
* @return integer one of the state constants.
*/
public static function manually_graded_state_for_fraction($fraction) {
if (is_null($fraction)) {
return self::$needsgrading;
} else if ($fraction < 0.000001) {
return self::$mangrwrong;
} else if ($fraction > 0.999999) {
return self::$mangrright;
} else {
return self::$mangrpartial;
}
}
/**
* Compute an appropriate state to move to after a manual comment has been
* added to this state.
* @param number $fraction the manual grade (if any) on the fraction scale.
* @return integer the new state.
*/
public function corresponding_commented_state($fraction) {
throw new Exception('Unexpected question state.');
}
/**
* Return an appropriate CSS class name ''/'correct'/'partiallycorrect'/'incorrect',
* for a state.
* @return string
*/
public function get_feedback_class() {
return '';
}
/**
* Return the name of an appropriate string to look up in the question
* language pack for a state. This is used, for example, by
* {@link question_behaviour::get_state_string()}. However, behaviours
* sometimes change this default string for soemthing more specific.
*
* @param boolean $showcorrectness Whether right/partial/wrong states should
* be distinguised, or just treated as 'complete'.
* @return string the name of a string that can be looked up in the 'question'
* lang pack, or used as a CSS class name, etc.
*/
public abstract function get_state_class($showcorrectness);
/**
* The result of doing get_string on the result of {@link get_state_class()}.
*
* @param boolean $showcorrectness Whether right/partial/wrong states should
* be distinguised.
* @return string a string from the lang pack that can be used in the UI.
*/
public function default_string($showcorrectness) {
return get_string($this->get_state_class($showcorrectness), 'question');
}
}
/**#@+
* Specific question_state subclasses.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_state_notstarted extends question_state {
public function is_finished() {
return false;
}
public function get_state_class($showcorrectness) {
throw new Exception('Unexpected question state.');
}
}
class question_state_unprocessed extends question_state {
public function is_finished() {
return false;
}
public function get_state_class($showcorrectness) {
throw new Exception('Unexpected question state.');
}
}
class question_state_todo extends question_state {
public function is_active() {
return true;
}
public function is_finished() {
return false;
}
public function get_state_class($showcorrectness) {
return 'notyetanswered';
}
}
class question_state_invalid extends question_state {
public function is_active() {
return true;
}
public function is_finished() {
return false;
}
public function get_state_class($showcorrectness) {
return 'invalidanswer';
}
}
class question_state_complete extends question_state {
public function is_active() {
return true;
}
public function is_finished() {
return false;
}
public function get_state_class($showcorrectness) {
return 'answersaved';
}
}
class question_state_needsgrading extends question_state {
public function get_state_class($showcorrectness) {
return 'requiresgrading';
}
public function corresponding_commented_state($fraction) {
return self::manually_graded_state_for_fraction($fraction);
}
}
class question_state_finished extends question_state {
public function get_state_class($showcorrectness) {
return 'complete';
}
public function corresponding_commented_state($fraction) {
return self::$manfinished;
}
}
class question_state_gaveup extends question_state {
public function is_gave_up() {
return true;
}
public function get_feedback_class() {
return 'incorrect';
}
public function get_state_class($showcorrectness) {
return 'notanswered';
}
public function corresponding_commented_state($fraction) {
if (is_null($fraction)) {
return self::$mangaveup;
} else {
return self::manually_graded_state_for_fraction($fraction);
}
}
}
abstract class question_state_graded extends question_state {
public function is_graded() {
return true;
}
public function get_state_class($showcorrectness) {
if ($showcorrectness) {
return $this->get_feedback_class();
} else {
return 'complete';
}
}
public function corresponding_commented_state($fraction) {
return self::manually_graded_state_for_fraction($fraction);
}
}
class question_state_gradedwrong extends question_state_graded {
public function is_incorrect() {
return true;
}
public function get_feedback_class() {
return 'incorrect';
}
}
class question_state_gradedpartial extends question_state_graded {
public function is_graded() {
return true;
}
public function is_partially_correct() {
return true;
}
public function get_feedback_class() {
return 'partiallycorrect';
}
}
class question_state_gradedright extends question_state_graded {
public function is_graded() {
return true;
}
public function is_correct() {
return true;
}
public function get_feedback_class() {
return 'correct';
}
}
class question_state_manfinished extends question_state_finished {
public function is_commented() {
return true;
}
}
class question_state_mangaveup extends question_state_gaveup {
public function is_commented() {
return true;
}
}
abstract class question_state_manuallygraded extends question_state_graded {
public function is_commented() {
return true;
}
}
class question_state_mangrwrong extends question_state_manuallygraded {
public function is_incorrect() {
return false;
}
public function get_feedback_class() {
return 'incorrect';
}
}
class question_state_mangrpartial extends question_state_manuallygraded {
public function is_partially_correct() {
return true;
}
public function get_feedback_class() {
return 'partiallycorrect';
}
}
class question_state_mangrright extends question_state_manuallygraded {
public function is_correct() {
return true;
}
public function get_feedback_class() {
return 'correct';
}
}
/**#@-*/
question_state::init();

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,111 @@
<?php
// This script fetches files from the dataroot/questionattempt directory
// It is based on the top-level file.php
//
// On a module-by-module basis (currently only implemented for quiz), it checks
// whether the user has permission to view the file.
//
// Syntax: question/file.php/attemptid/questionid/filename.ext
// Workaround: question/file.php?file=/attemptid/questionid/filename.ext
// 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 script fetches files from the dataroot/questionattempt directory
* It is based on the top-level file.php
*
* On a module-by-module basis (currently only implemented for quiz), it checks
* whether the user has permission to view the file.
*
* Syntax: question/file.php/attemptid/questionid/filename.ext
* Workaround: question/file.php?file=/attemptid/questionid/filename.ext
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2007 Adriane Boyd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// disable moodle specific debug messages and any errors in output
define('NO_DEBUG_DISPLAY', true);
require_once('../config.php');
require_once('../lib/filelib.php');
$relativepath = get_file_argument();
// force download for any student-submitted files to prevent XSS attacks.
$forcedownload = 1;
require_once('../config.php');
require_once('../lib/filelib.php');
// relative path must start with '/', because of backup/restore!!!
if (!$relativepath) {
print_error('invalidarguments');
} else if ($relativepath{0} != '/') {
print_error('pathdoesnotstartslash');
$relativepath = get_file_argument();
// force download for any student-submitted files to prevent XSS attacks.
$forcedownload = 1;
// relative path must start with '/', because of backup/restore!!!
if (!$relativepath) {
print_error('invalidarguments');
} else if ($relativepath{0} != '/') {
print_error('pathdoesnotstartslash');
}
$pathname = $CFG->dataroot.'/questionattempt'.$relativepath;
// extract relative path components
$args = explode('/', trim($relativepath, '/'));
// check for the right number of directories in the path
if (count($args) != 3) {
print_error('invalidarguments');
}
// security: require login
require_login();
// security: do not return directory node!
if (is_dir($pathname)) {
question_attempt_not_found();
}
$lifetime = 0; // do not cache because students may reupload files
// security: check that the user has permission to access this file
$haspermission = false;
if ($attempt = $DB->get_record("question_attempts", array("id" => $args[0]))) {
$modfile = $CFG->dirroot .'/mod/'. $attempt->modulename .'/lib.php';
$modcheckfileaccess = $attempt->modulename .'_check_file_access';
if (file_exists($modfile)) {
@require_once($modfile);
if (function_exists($modcheckfileaccess)) {
$haspermission = $modcheckfileaccess($args[0], $args[1]);
}
}
$pathname = $CFG->dataroot.'/questionattempt'.$relativepath;
// extract relative path components
$args = explode('/', trim($relativepath, '/'));
// check for the right number of directories in the path
if (count($args) != 3) {
print_error('invalidarguments');
} else if ($args[0][0] == 0) {
global $USER;
$list = explode('_', $args[0]);
if ($list[1] == $USER->id) {
$haspermission = true;
}
}
// security: require login
require_login();
// security: do not return directory node!
if (is_dir($pathname)) {
if ($haspermission) {
// check that file exists
if (!file_exists($pathname)) {
question_attempt_not_found();
}
$lifetime = 0; // do not cache because students may reupload files
// security: check that the user has permission to access this file
$haspermission = false;
if ($attempt = $DB->get_record("question_attempts", array("id" => $args[0]))) {
$modfile = $CFG->dirroot .'/mod/'. $attempt->modulename .'/lib.php';
$modcheckfileaccess = $attempt->modulename .'_check_file_access';
if (file_exists($modfile)) {
@require_once($modfile);
if (function_exists($modcheckfileaccess)) {
$haspermission = $modcheckfileaccess($args[0], $args[1]);
}
}
} else if ($args[0][0] == 0) {
global $USER;
$list = explode('_', $args[0]);
if ($list[1] == $USER->id) {
$haspermission = true;
}
}
if ($haspermission) {
// check that file exists
if (!file_exists($pathname)) {
question_attempt_not_found();
}
// send the file
session_get_instance()->write_close(); // unlock session during fileserving
$filename = $args[count($args)-1];
send_file($pathname, $filename, $lifetime, $CFG->filteruploadedfiles, false, $forcedownload);
} else {
question_attempt_not_found();
}
function question_attempt_not_found() {
global $CFG;
header('HTTP/1.0 404 not found');
print_error('filenotfound', 'error', $CFG->wwwroot); //this is not displayed on IIS??
}
// send the file
session_get_instance()->write_close(); // unlock session during fileserving
$filename = $args[count($args)-1];
send_file($pathname, $filename, $lifetime, $CFG->filteruploadedfiles, false, $forcedownload);
} else {
question_attempt_not_found();
}
function question_attempt_not_found() {
global $CFG;
header('HTTP/1.0 404 not found');
print_error('filenotfound', 'error', $CFG->wwwroot); //this is not displayed on IIS??
}

0
question/format/examview/format.php Executable file → Normal file
View File

0
question/format/gift/format.php Executable file → Normal file
View File

0
question/format/learnwise/format.php Executable file → Normal file
View File

0
question/format/qti_two/templates/choice.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/choiceMultiple.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/composite.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/extendedText.tpl Executable file → Normal file
View File

View File

0
question/format/qti_two/templates/imsmanifest.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/mmchoiceMultiple.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/notimplemented.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/numerical.tpl Executable file → Normal file
View File

0
question/format/qti_two/templates/textEntry.tpl Executable file → Normal file
View File

0
question/format/xhtml/format.php Executable file → Normal file
View File

0
question/format/xml/format.php Executable file → Normal file
View File

View File

@ -78,5 +78,23 @@ class question_import_form extends moodleform {
$template = "{help} {element}\n";
$renderer->setGroupElementTemplate($template, 'format');
}
public function validation($data, $files){
$errors = parent::validation($data, $files);
return $errors; // TODO.
// If a file is uploaded return
if ($data['choosefile'] || $files['newfile']) {
return $errors;
}
// If file is not uploaded from file upload
if (!$files['newfile']) {
$errors['newfile'] = get_string('importfromupload', 'question');
}
// If filke is not imported from course files
if (!$data['choosefile'] && !$files['newfile']) {
$errors['choosefile'] = get_string('importfromcoursefiles', 'question');
}
return $errors;
}
}

View File

@ -1,5 +1,30 @@
<?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/>.
/**
* Form for moving questions between categories.
*
* @package moodlecore
* @subpackage questionbank
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
@ -7,22 +32,17 @@ if (!defined('MOODLE_INTERNAL')) {
require_once($CFG->libdir.'/formslib.php');
class question_move_form extends moodleform {
function definition() {
$mform =& $this->_form;
$mform = $this->_form;
$currentcat = $this->_customdata['currentcat'];
$contexts = $this->_customdata['contexts'];
//--------------------------------------------------------------------------------
$currentcat = $this->_customdata['currentcat'];
$contexts = $this->_customdata['contexts'];
$mform->addElement('questioncategory', 'category', get_string('category','quiz'), compact('contexts', 'currentcat'));
//--------------------------------------------------------------------------------
$this->add_action_buttons(true, get_string('categorymoveto', 'quiz'));
//--------------------------------------------------------------------------------
$mform->addElement('hidden', 'delete', $currentcat);
$mform->setType('delete', PARAM_INT);
}
}

46
question/preview.js Normal file
View File

@ -0,0 +1,46 @@
// 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 file the Moodle question engine.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Initialise JavaScript-specific parts of the question preview popup.
*/
function question_preview_init(caption, addto) {
// Add a close button to the window.
var button = document.createElement('input');
button.type = 'button';
button.value = caption;
YAHOO.util.Event.addListener(button, 'click', function() { window.close() });
var container = document.getElementById(addto);
container.appendChild(button);
// Make changint the settings disable all submit buttons, like clicking one of the
// question buttons does.
var form = document.getElementById('mform1');
YAHOO.util.Event.addListener(form, 'submit',
question_prevent_repeat_submission, document.body);
}

0
question/type/calculated/pix/icon.gif Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 78 B

After

Width:  |  Height:  |  Size: 78 B

View File

@ -0,0 +1,92 @@
<?php
///////////////////////////////////////////////////////////////////////////
// //
// NOTICE OF COPYRIGHT //
// //
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
// http://moodle.org //
// //
// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
// //
// This program 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 2 of the License, or //
// (at your option) any later version. //
// //
// This program 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: //
// //
// http://www.gnu.org/copyleft/gpl.html //
// //
///////////////////////////////////////////////////////////////////////////
/**
* The description question type is not acutally a question, it is just a way
* to add some static content in the middle of a quiz, or other place that
* questions are used.
*
* @package questionbank
* @subpackage questiontypes
*/
class description_qtype extends default_questiontype {
function name() {
return 'description';
}
function is_real_question_type() {
return false;
}
function is_usable_by_random() {
return false;
}
function save_question($question, $form) {
// Make very sure that descriptions can'e be created with a grade of
// anything other than 0.
$form->defaultgrade = 0;
return parent::save_question($question, $form);
}
function get_question_options(&$question) {
return true;
}
function save_question_options($question) {
return true;
}
function print_question(&$question, &$state, $number, $cmoptions, $options) {
global $CFG;
$isfinished = question_state_is_graded($state->last_graded) || $state->event == QUESTION_EVENTCLOSE;
// For editing teachers print a link to an editing popup window
$editlink = $this->get_question_edit_link($question, $cmoptions, $options);
$questiontext = $this->format_text($question->questiontext, $question->questiontextformat, $cmoptions);
$generalfeedback = '';
if ($isfinished && $options->generalfeedback) {
$generalfeedback = $this->format_text($question->generalfeedback,
$question->generalfeedbackformat, $cmoptions);
}
include "$CFG->dirroot/question/type/description/question.html";
}
function actual_number_of_questions($question) {
return 0;
}
function grade_responses(&$question, &$state, $cmoptions) {
$state->raw_grade = 0;
$state->penalty = 0;
return true;
}
}
// Register this question type with questionlib.php.
question_register_questiontype(new description_qtype());

View File

@ -0,0 +1,35 @@
<?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/>.
/**
* Description 'question' definition class.
*
* @package qtype_description
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Represents a description 'question'.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_description_question extends question_information_item {
}

View File

@ -1,95 +1,73 @@
<?php
///////////////////////////////////////////////////////////////////////////
// //
// NOTICE OF COPYRIGHT //
// //
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
// http://moodle.org //
// //
// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
// //
// This program 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 2 of the License, or //
// (at your option) any later version. //
// //
// This program 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: //
// //
// http://www.gnu.org/copyleft/gpl.html //
// //
///////////////////////////////////////////////////////////////////////////
// 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/>.
/**
* The description question type is not acutally a question, it is just a way
* to add some static content in the middle of a quiz, or other place that
* questions are used.
* Question type class for the description 'question' type.
*
* @package questionbank
* @subpackage questiontypes
* @package qtype_description
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class description_qtype extends default_questiontype {
function name() {
return 'description';
}
require_once($CFG->libdir . '/questionlib.php');
function is_real_question_type() {
/**
* The description 'question' type.
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_description extends question_type {
public function is_usable_by_random() {
return false;
}
function is_usable_by_random() {
public function can_analyse_responses() {
return false;
}
function save_question($question, $form) {
public function save_question($question, $form, $course) {
// Make very sure that descriptions can'e be created with a grade of
// anything other than 0.
$form->defaultgrade = 0;
return parent::save_question($question, $form);
$form->defaultmark = 0;
return parent::save_question($question, $form, $course);
}
function get_question_options(&$question) {
public function get_question_options(&$question) {
// No options to be restored for this question type
return true;
}
function save_question_options($question) {
public function save_question_options($question) {
/// No options to be saved for this question type:
return true;
}
function print_question(&$question, &$state, $number, $cmoptions, $options) {
global $CFG;
$isfinished = question_state_is_graded($state->last_graded) || $state->event == QUESTION_EVENTCLOSE;
// For editing teachers print a link to an editing popup window
$editlink = $this->get_question_edit_link($question, $cmoptions, $options);
$context = $this->get_context_by_category_id($question->category);
$question->questiontext = quiz_rewrite_question_urls($question->questiontext, 'pluginfile.php', $context->id, 'question', 'questiontext', array($state->attempt, $state->question), $question->id);
$questiontext = $this->format_text($question->questiontext, $question->questiontextformat, $cmoptions);
$generalfeedback = '';
if ($isfinished && $options->generalfeedback) {
$question->generalfeedback = quiz_rewrite_question_urls($question->generalfeedback, 'pluginfile.php', $context->id, 'question', 'generalfeedback', array($state->attempt, $state->question), $question->id);
$generalfeedback = $this->format_text($question->generalfeedback,
$question->generalfeedbackformat, $cmoptions);
}
include "$CFG->dirroot/question/type/description/question.html";
}
function actual_number_of_questions($question) {
public function actual_number_of_questions($question) {
/// Used for the feature number-of-questions-per-page
/// to determine the actual number of questions wrapped
/// by this question.
/// The question type description is not even a question
/// in itself so it will return ZERO!
return 0;
}
function grade_responses(&$question, &$state, $cmoptions) {
$state->raw_grade = 0;
$state->penalty = 0;
return true;
public function get_random_guess_score($questiondata) {
return null;
}
}
// Register this question type with questionlib.php.
question_register_questiontype(new description_qtype());

View File

@ -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/>.
/**
* Description 'question' renderer class.
*
* @package qtype_description
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Generates the output for description 'question's.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_description_renderer extends qtype_renderer {
public function formulation_and_controls(question_attempt $qa,
question_display_options $options) {
return html_writer::tag('div', $qa->get_question()->format_questiontext(),
array('class' => 'qtext'));
}
}

View File

@ -0,0 +1,78 @@
<?php
///////////////////
/// missingtype ///
///////////////////
/// QUESTION TYPE CLASS //////////////////
/**
* Missing question type class
*
* When a question is encountered with a type that is not installed then its
* type is changed to 'missingtype'. This questiontype just makes sure that the
* necessary information is printed about the question.
* @package questionbank
* @subpackage questiontypes
*/
class question_missingtype_qtype extends default_questiontype {
function name() {
return 'missingtype';
}
function menu_name() {
return false;
}
function is_usable_by_random() {
return false;
}
function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
global $CFG;
$answers = &$question->options->answers;
$formatoptions = new stdClass;
$formatoptions->noclean = true;
$formatoptions->para = false;
// Print formulation
$questiontext = format_text($question->questiontext,
$question->questiontextformat,
$formatoptions, $cmoptions->course);
// Print each answer in a separate row if there are any
$anss = array();
if ($answers) {
foreach ($answers as $answer) {
$a = new stdClass;
$a->text = format_text($answer->answer, $answer->answerformat, $formatoptions, $cmoptions->course);
$anss[] = clone($a);
}
}
include("$CFG->dirroot/question/type/missingtype/display.html");
}
function grade_responses(&$question, &$state, $cmoptions) {
return true;
}
function display_question_editing_page(&$mform, $question, $wizardnow){
global $OUTPUT;
echo $OUTPUT->heading(get_string('warningmissingtype', 'qtype_missingtype'));
$mform->display();
}
}
//// END OF CLASS ////
//////////////////////////////////////////////////////////////////////////
//// INITIATION - Without this line the question type is not in use... ///
//////////////////////////////////////////////////////////////////////////
question_register_questiontype(new question_missingtype_qtype());

View File

@ -0,0 +1,86 @@
<?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 'qtype_missingtype' question definition class.
*
* @package qtype_missingtype
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* This question definition class is used when the actual question type of this
* question cannot be found.
*
* Why does this this class implement question_automatically_gradable? I am not
* sure at the moment. Perhaps it is important for it to work with as many
* behaviours as possible.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_missingtype_question extends question_definition implements question_automatically_gradable {
public function get_expected_data() {
return array();
}
public function get_correct_response() {
return array();
}
public function is_complete_response(array $response) {
return false;
}
public function is_gradable_response(array $response) {
return false;
}
public function get_validation_error(array $response) {
return '';
}
public function is_same_response(array $prevresponse, array $newresponse) {
return true;
}
public function get_right_answer_summary() {
return '';
}
public function summarise_response(array $response) {
return null;
}
public function classify_response(array $response) {
return array();
}
public function init_first_step(question_attempt_step $step) {
}
public function grade_response(array $response) {
throw new Exception('This question is of a type that is not installed on your system. No processing is possible.');
}
public function get_hint($hintnumber, question_attempt $qa) {
return null;
}
}

View File

@ -1,78 +1,92 @@
<?php
///////////////////
/// missingtype ///
///////////////////
// 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 type class for the 'missingtype' type.
*
* @package qtype_missingtype
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/// QUESTION TYPE CLASS //////////////////
/**
* Missing question type class
*
* When a question is encountered with a type that is not installed then its
* type is changed to 'missingtype'. This questiontype just makes sure that the
* necessary information is printed about the question.
* @package questionbank
* @subpackage questiontypes
* When we encounter a question of a type that is not currently installed, then
* we use this question type class instead so that some of the information about
* this question can be seen, and the rest of the system keeps working.
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_missingtype_qtype extends default_questiontype {
function name() {
return 'missingtype';
}
function menu_name() {
class qtype_missingtype extends question_type {
public function menu_name() {
return false;
}
function is_usable_by_random() {
public function is_usable_by_random() {
return false;
}
function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
global $CFG;
$answers = &$question->options->answers;
$formatoptions = new stdClass;
$formatoptions->noclean = true;
$formatoptions->para = false;
// Print formulation
$questiontext = format_text($question->questiontext,
$question->questiontextformat,
$formatoptions, $cmoptions->course);
// Print each answer in a separate row if there are any
$anss = array();
if ($answers) {
foreach ($answers as $answer) {
$a = new stdClass;
$a->text = format_text($answer->answer, $answer->answerformat, $formatoptions, $cmoptions->course);
$anss[] = clone($a);
}
}
include("$CFG->dirroot/question/type/missingtype/display.html");
public function can_analyse_responses() {
return false;
}
function grade_responses(&$question, &$state, $cmoptions) {
return true;
public function make_question($questiondata) {
$question = parent::make_question($questiondata);
$question->questiontext = html_writer::tag('div',
get_string('missingqtypewarning', 'qtype_missingtype'),
array('class' => 'warning missingqtypewarning')) .
$question->questiontext;
return $question;
}
function display_question_editing_page(&$mform, $question, $wizardnow){
global $OUTPUT;
echo $OUTPUT->heading(get_string('warningmissingtype', 'qtype_missingtype'));
public function make_deleted_instance($questionid, $maxmark) {
question_bank::load_question_definition_classes('missingtype');
$question = new qtype_missingtype_question();
$question->id = $questionid;
$question->category = null;
$question->parent = 0;
$question->qtype = question_bank::get_qtype('missingtype');
$question->name = get_string('deletedquestion', 'qtype_missingtype');
$question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');
$question->questiontextformat = FORMAT_HTML;
$question->generalfeedback = '';
$question->defaultmark = $maxmark;
$question->length = 1;
$question->penalty = 0;
$question->stamp = '';
$question->version = 0;
$question->hidden = 0;
$question->timecreated = null;
$question->timemodified = null;
$question->createdby = null;
$question->modifiedby = null;
return $question;
}
public function get_random_guess_score($questiondata) {
return null;
}
public function display_question_editing_page($mform, $question, $wizardnow){
print_heading(get_string('missingqtypewarning', 'qtype_missingtype'));
$mform->display();
}
}
//// END OF CLASS ////
//////////////////////////////////////////////////////////////////////////
//// INITIATION - Without this line the question type is not in use... ///
//////////////////////////////////////////////////////////////////////////
question_register_questiontype(new question_missingtype_qtype());

View File

@ -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/>.
/**
* Defines the 'missingtype' question renderer class.
*
* @package qtype_missingtype
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* This question renderer class is used when the actual question type of this
* question cannot be found.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_missingtype_renderer extends qtype_renderer {
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,787 @@
<?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 file defines the class {@link question_definition} and its subclasses.
*
* @package moodlecore
* @subpackage questiontypes
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* The definition of a question of a particular type.
*
* This class is a close match to the question table in the database.
* Definitions of question of a particular type normally subclass one of the
* more specific classes {@link question_with_responses},
* {@link question_graded_automatically} or {@link question_information_item}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_definition {
/** @var integer id of the question in the datase, or null if this question
* is not in the database. */
public $id;
/** @var integer question category id. */
public $category;
/** @var integer parent question id. */
public $parent = 0;
/** @var question_type the question type this question is. */
public $qtype;
/** @var string question name. */
public $name;
/** @var string question text. */
public $questiontext;
/** @var integer question test format. */
public $questiontextformat;
/** @var string question general feedback. */
public $generalfeedback;
/** @var number what this quetsion is marked out of, by default. */
public $defaultmark = 1;
/** @var integer How many question numbers this question consumes. */
public $length = 1;
/** @var number penalty factor of this question. */
public $penalty = 0;
/** @var string unique identifier of this question. */
public $stamp;
/** @var string unique identifier of this version of this question. */
public $version;
/** @var boolean whethre this question has been deleted/hidden in the question bank. */
public $hidden = 0;
/** @var integer timestamp when this question was created. */
public $timecreated;
/** @var integer timestamp when this question was modified. */
public $timemodified;
/** @var integer userid of the use who created this question. */
public $createdby;
/** @var integer userid of the use who modified this question. */
public $modifiedby;
/** @var array of question_hints. */
public $hints = array();
/**
* Constructor. Normally to get a question, you call
* {@link question_bank::load_question()}, but questions can be created
* directly, for example in unit test code.
* @return unknown_type
*/
public function __construct() {
}
/**
* @return the name of the question type (for example multichoice) that this
* question is.
*/
public function get_type_name() {
return $this->qtype->name();
}
/**
* Creat the appropriate behaviour for an attempt at this quetsion,
* given the desired (archetypal) behaviour.
*
* This default implementation will suit most normal graded questions.
*
* If your question is of a patricular type, then it may need to do something
* different. For example, if your question can only be graded manually, then
* it should probably return a manualgraded behaviour, irrespective of
* what is asked for.
*
* If your question wants to do somthing especially complicated is some situations,
* then you may wish to return a particular behaviour related to the
* one asked for. For example, you migth want to return a
* qbehaviour_interactive_adapted_for_myqtype.
*
* @param question_attempt $qa the attempt we are creating an behaviour for.
* @param string $preferredbehaviour the requested type of behaviour.
* @return question_behaviour the new behaviour object.
*/
public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
}
/**
* Initialise the first step of an attempt at this quetsion.
*
* For example, the multiple choice question type uses this method to
* randomly shuffle the choices, if that option has been set in the question.
* It then stores that order by calling $step->set_qt_var(...).
*
* @param question_attempt_step $step the step to be initialised.
*/
public function init_first_step(question_attempt_step $step) {
}
/**
* 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.
* @return string|null a plain text summary of this question.
*/
public function get_question_summary() {
return html_to_text($this->format_questiontext(), 0, false);
}
/**
* Some questions can return a negative mark if the student gets it wrong.
*
* This method returns the lowest mark the question can return, on the
* fraction scale. that is, where the maximum possible mark is 1.0.
*
* @return number minimum fraction this question will ever return.
*/
public function get_min_fraction() {
return 0;
}
/**
* Given a response, rest the parts that are wrong.
* @param array $response a response
* @return array a cleaned up response with the wrong bits reset.
*/
public function clear_wrong_from_response(array $response) {
return array();
}
/**
* Return the number of subparts of this response that are right.
* @param array $response a response
* @return array with two elements, the number of correct subparts, and
* the total number of subparts.
*/
public function get_num_parts_right(array $response) {
return array(null, null);
}
/**
* @return qtype_renderer the renderer to use for outputting this question.
*/
public function get_renderer() {
return renderer_factory::get_renderer('qtype_' . $this->qtype->name());
}
/**
* What data may be included in the form submission when a student submits
* this question in its current state?
*
* This information is used in calls to optional_param. The parameter name
* has {@link question_attempt::get_field_prefix()} automatically prepended.
*
* @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 abstract function get_expected_data();
/**
* What data would need to be submitted to get this question correct.
* If there is more than one correct answer, this method should just
* return one possibility.
*
* @return array parameter name => value.
*/
public abstract function get_correct_response();
/**
* Apply {@link format_text()} to some content with appropriate settings for
* this question.
*
* @param string $text some content that needs to be output.
* @param boolean $clean Whether the HTML needs to be cleaned. Generally,
* parts of the question do not need to be cleaned, and student input does.
* @return string the text formatted for output by format_text.
*/
public function format_text($text, $clean = false) {
$formatoptions = new stdClass;
$formatoptions->noclean = !$clean;
$formatoptions->para = false;
return format_text($text, $this->questiontextformat, $formatoptions);
}
/** @return the result of applying {@link format_text()} to the question text. */
public function format_questiontext() {
return $this->format_text($this->questiontext);
}
/** @return the result of applying {@link format_text()} to the general feedback. */
public function format_generalfeedback() {
return $this->format_text($this->generalfeedback);
}
}
/**
* This class represents a 'question' that actually does not allow the student
* to respond, like the description 'question' type.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_information_item extends question_definition {
public function __construct() {
parent::__construct();
$this->defaultmark = 0;
$this->penalty = 0;
$this->length = 0;
}
public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
question_engine::load_behaviour_class('informationitem');
return new qbehaviour_informationitem($qa, $preferredbehaviour);
}
public function get_expected_data() {
return array();
}
public function get_correct_response() {
return array();
}
public function get_question_summary() {
return null;
}
}
/**
* Interface that a {@link question_definition} must implement to be usable by
* the manual graded behaviour.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface question_manually_gradable {
/**
* Used by many of the behaviours, to work out whether the student's
* response to the question is complete. That is, whether the question attempt
* should move to the COMPLETE or INCOMPLETE state.
*
* @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
* @return boolean whether this response is a complete answer to this question.
*/
public function is_complete_response(array $response);
/**
* Use by many of the behaviours to determine whether the student's
* response has changed. This is normally used to determine that a new set
* of responses can safely be discarded.
*
* @param array $prevresponse the responses previously recorded for this question,
* as returned by {@link question_attempt_step::get_qt_data()}
* @param array $newresponse the new responses, in the same format.
* @return boolean whether the two sets of responses are the same - that is
* whether the new set of responses can safely be discarded.
*/
public function is_same_response(array $prevresponse, array $newresponse);
/**
* Produce a plain text summary of a response.
* @param $response a response, as might be passed to {@link grade_response()}.
* @return string a plain text summary of that response, that could be used in reports.
*/
public function summarise_response(array $response);
/**
* Categorise the student's response according to the categories defined by
* get_possible_responses.
* @param $response a response, as might be passed to {@link grade_response()}.
* @return array subpartid => {@link question_classified_response} objects.
* returns an empty array if no analysis is possible.
*/
public function classify_response(array $response);
}
/**
* This class is used in the return value from
* {@link question_manually_gradable::classify_response()}.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_classified_response {
/**
* @var string the classification of this response the student gave to this
* part of the question. Must match one of the responseclasses returned by
* {@link question_type::get_possible_responses()}.
*/
public $responseclassid;
/** @var string the actual response the student gave to this part. */
public $response;
/** @var number the fraction this part of the response earned. */
public $fraction;
/**
* Constructor, just an easy way to set the fields.
* @param string $responseclassid see the field descriptions above.
* @param string $response see the field descriptions above.
* @param number $fraction see the field descriptions above.
*/
public function __construct($responseclassid, $response, $fraction) {
$this->responseclassid = $responseclassid;
$this->response = $response;
$this->fraction = $fraction;
}
public static function no_response() {
return new question_classified_response(null, null, null);
}
}
/**
* Interface that a {@link question_definition} must implement to be usable by
* the various automatic grading behaviours.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface question_automatically_gradable extends question_manually_gradable {
/**
* Use by many of the behaviours to determine whether the student
* has provided enough of an answer for the question to be graded automatically,
* or whether it must be considered aborted.
*
* @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
* @return boolean whether this response can be graded.
*/
public function is_gradable_response(array $response);
/**
* In situations where is_gradable_response() returns false, this method
* should generate a description of what the problem is.
* @return string the message.
*/
public function get_validation_error(array $response);
/**
* Grade a response to the question, returning a fraction between get_min_fraction() and 1.0,
* and the corresponding state CORRECT, PARTIALLY_CORRECT or INCORRECT.
* @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}.
* @return array (number, integer) the fraction, and the state.
*/
public function grade_response(array $response);
/**
* Get one of the question hints. The question_attempt is passed in case
* the question type wants to do something complex. For example, the
* multiple choice with multiple responses question type will turn off most
* of the hint options if the student has selected too many opitions.
* @param integer $hintnumber Which hint to display. Indexed starting from 0
* @param question_attempt $qa The question_attempt.
*/
public function get_hint($hintnumber, question_attempt $qa);
/**
* 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();
}
/**
* Interface that a {@link question_definition} must implement to be usable by
* the interactivecountback behaviour.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface question_automatically_gradable_with_countback extends question_automatically_gradable {
/**
* Work out a final grade for this attempt, taking into account all the
* tries the student made.
* @param array $responses the response for each try. Each element of this
* array is a response array, as would be passed to {@link grade_response()}.
* There may be between 1 and $totaltries responses.
* @param integer $totaltries The maximum number of tries allowed.
* @return numeric the fraction that should be awarded for this
* sequence of response.
*/
public function compute_final_grade($responses, $totaltries);
}
/**
* This class represents a real question. That is, one that is not a
* {@link question_information_item}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_with_responses extends question_definition
implements question_manually_gradable {
function classify_response(array $response) {
return array();
}
}
/**
* This class represents a question that can be graded automatically.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_graded_automatically extends question_with_responses
implements question_automatically_gradable {
/** @var Some question types have the option to show the number of sub-parts correct. */
public $shownumcorrect = false;
public function is_gradable_response(array $response) {
return $this->is_complete_response($response);
}
public function get_right_answer_summary() {
$correctresponse = $this->get_correct_response();
if (empty($correctresponse)) {
return null;
}
return $this->summarise_response($correctresponse);
}
public function get_hint($hintnumber, question_attempt $qa) {
if (!isset($this->hints[$hintnumber])) {
return null;
}
return $this->hints[$hintnumber];
}
}
/**
* This class represents a question that can be graded automatically with
* countback grading in interactive mode.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_graded_automatically_with_countback
extends question_graded_automatically
implements question_automatically_gradable_with_countback {
public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
if ($preferredbehaviour == 'interactive') {
return question_engine::make_behaviour('interactivecountback', $qa, $preferredbehaviour);
}
return question_engine::make_archetypal_behaviour($preferredbehaviour, $qa);
}
}
/**
* This class represents a question that can be graded automatically by using
* a {@link question_grading_strategy}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_graded_by_strategy extends question_graded_automatically {
/** @var question_grading_strategy the strategy to use for grading. */
protected $gradingstrategy;
/** @param question_grading_strategy $strategy the strategy to use for grading. */
public function __construct(question_grading_strategy $strategy) {
parent::__construct();
$this->gradingstrategy = $strategy;
}
public function get_correct_response() {
$answer = $this->get_correct_answer();
if (!$answer) {
return array();
}
return array('answer' => $answer->answer);
}
/**
* Get an answer that contains the feedback and fraction that should be
* awarded for this resonse.
* @param array $response a response.
* @return question_answer the matching answer.
*/
public function get_matching_answer(array $response) {
return $this->gradingstrategy->grade($response);
}
/**
* @return question_answer an answer that contains the a response that would
* get full marks.
*/
public function get_correct_answer() {
return $this->gradingstrategy->get_correct_answer();
}
public function grade_response(array $response) {
$answer = $this->get_matching_answer($response);
if ($answer) {
return array($answer->fraction, question_state::graded_state_for_fraction($answer->fraction));
} else {
return array(0, question_state::$gradedwrong);
}
}
public function classify_response(array $response) {
if (empty($response['answer'])) {
return array($this->id => question_classified_response::no_response());
}
$ans = $this->get_matching_answer($response);
if (!$ans) {
return array($this->id => question_classified_response::no_response());
}
return array($this->id => new question_classified_response(
$ans->id, $response['answer'], $ans->fraction));
}
}
/**
* Class to represent a question answer, loaded from the question_answers table
* in the database.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_answer {
/** @var string the answer. */
public $answer;
/** @var number the fraction this answer is worth. */
public $fraction;
/** @var string the feedback for this answer. */
public $feedback;
/**
* Constructor.
* @param string $answer the answer.
* @param number $fraction the fraction this answer is worth.
* @param string $feedback the feedback for this answer.
*/
public function __construct($answer, $fraction, $feedback) {
$this->answer = $answer;
$this->fraction = $fraction;
$this->feedback = $feedback;
}
}
/**
* Class to represent a hint associated with a question.
* Used by iteractive mode, etc. A question has an array of these.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_hint {
/** @var The feedback hint to be shown. */
public $hint;
/**
* Constructor.
* @param string $hint The hint text
*/
public function __construct($hint) {
$this->hint = $hint;
}
/**
* Create a basic hint from a row loaded from the question_hints table in the database.
* @param object $row with $row->hint set.
* @return question_hint
*/
public static function load_from_record($row) {
return new question_hint($row->hint);
}
/**
* Adjust this display options according to the hint settings.
* @param question_display_options $options
*/
public function adjust_display_options(question_display_options $options) {
// Do nothing.
}
}
/**
* An extension of {@link question_hint} for questions like match and multiple
* choice with multile answers, where there are options for whether to show the
* number of parts right at each stage, and to reset the wrong parts.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_hint_with_parts extends question_hint {
/** @var boolean option to show the number of sub-parts of the question that were right. */
public $shownumcorrect;
/** @var boolean option to clear the parts of the question that were wrong on retry. */
public $clearwrong;
/**
* Constructor.
* @param string $hint The hint text
* @param boolean $shownumcorrect whether the number of right parts should be shown
* @param boolean $clearwrong whether the wrong parts should be reset.
*/
public function __construct($hint, $shownumcorrect, $clearwrong) {
parent::__construct($hint);
$this->shownumcorrect = $shownumcorrect;
$this->clearwrong = $clearwrong;
}
/**
* Create a basic hint from a row loaded from the question_hints table in the database.
* @param object $row with $row->hint, ->shownumcorrect and ->clearwrong set.
* @return question_hint_with_parts
*/
public static function load_from_record($row) {
return new question_hint_with_parts($row->hint, $row->shownumcorrect, $row->clearwrong);
}
public function adjust_display_options(question_display_options $options) {
parent::adjust_display_options($options);
if ($this->clearwrong) {
$options->clearwrong = true;
}
$options->numpartscorrect = $this->shownumcorrect;
}
}
/**
* This question_grading_strategy interface. Used to share grading code between
* questions that that subclass {@link question_graded_by_strategy}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface question_grading_strategy {
/**
* Return a question answer that describes the outcome (fraction and feeback)
* for a particular respons.
* @param array $response the response.
* @return question_answer the answer describing the outcome.
*/
public function grade(array $response);
/**
* @return question_answer an answer that contains the a response that would
* get full marks.
*/
public function get_correct_answer();
}
/**
* This interface defines the methods that a {@link question_definition} must
* implement if it is to be graded by the
* {@link question_first_matching_answer_grading_strategy}.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface question_response_answer_comparer {
/** @return array of {@link question_answers}. */
public function get_answers();
/**
* @param array $response the response.
* @param question_answer $answer an answer.
* @return boolean whether the response matches the answer.
*/
public function compare_response_with_answer(array $response, question_answer $answer);
}
/**
* This grading strategy is used by question types like shortanswer an numerical.
* It gets a list of possible answers from the question, and returns the first one
* that matches the given response. It returns the first answer with fraction 1.0
* when asked for the correct answer.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_first_matching_answer_grading_strategy implements question_grading_strategy {
/**
* @var question_response_answer_comparer (presumably also a
* {@link question_definition}) the question we are doing the grading for.
*/
protected $question;
/**
* @param question_response_answer_comparer $question (presumably also a
* {@link question_definition}) the question we are doing the grading for.
*/
public function __construct(question_response_answer_comparer $question) {
$this->question = $question;
}
public function grade(array $response) {
foreach ($this->question->get_answers() as $aid => $answer) {
if ($this->question->compare_response_with_answer($response, $answer)) {
$answer->id = $aid;
return $answer;
}
}
return null;
}
public function get_correct_answer() {
foreach ($this->question->get_answers() as $answer) {
$state = question_state::graded_state_for_fraction($answer->fraction);
if ($state == question_state::$gradedright) {
return $answer;
}
}
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More