Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

MDL-28219 QE2 adaptive behaviour: fix scores and penalties

  • Loading branch information...
commit cc9fc649a155be2571daec7eded1a88418f11899 1 parent 6731a04
@bostelm bostelm authored
View
73 question/behaviour/adaptive/behaviour.php
@@ -127,16 +127,24 @@ public function process_submit(question_attempt_pending_step $pendingstep) {
return $status;
}
+ $prevstep = $this->qa->get_last_step_with_behaviour_var('_try');
+ $prevresponse = $prevstep->get_qt_data();
$prevtries = $this->qa->get_last_behaviour_var('_try', 0);
$prevbest = $pendingstep->get_fraction();
if (is_null($prevbest)) {
$prevbest = 0;
}
+ if ($this->question->is_same_response($response, $prevresponse)) {
+ return question_attempt::DISCARD;
+ }
+
list($fraction, $state) = $this->question->grade_response($response);
$pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
- if ($state == question_state::$gradedright) {
+ if ($prevstep->get_state() == question_state::$complete) {
+ $pendingstep->set_state(question_state::$complete);
+ } else if ($state == question_state::$gradedright) {
$pendingstep->set_state(question_state::$complete);
} else {
$pendingstep->set_state(question_state::$todo);
@@ -153,32 +161,59 @@ public function process_finish(question_attempt_pending_step $pendingstep) {
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();
+ $prevbest = $this->qa->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;
- }
+ $laststep = $this->qa->get_last_step();
+ $response = $laststep->get_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $state = question_state::$gaveup;
+ $fraction = 0;
+ } else {
- list($fraction, $state) = $this->question->grade_response($response);
+ if ($laststep->has_behaviour_var('_try')) {
+ // Last answer was graded, we want to regrade it. Otherwise the answer
+ // has changed, and we are grading a new try.
+ $prevtries -= 1;
+ }
+
+ list($fraction, $state) = $this->question->grade_response($response);
+
+ $pendingstep->set_behaviour_var('_try', $prevtries + 1);
+ $pendingstep->set_behaviour_var('_rawfraction', $fraction);
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ }
- $pendingstep->set_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));
+ $pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
return question_attempt::KEEP;
}
+
+ /**
+ * Got the most recently graded step. This is mainly intended for use by the
+ * renderer.
+ * @return question_attempt_step the most recently graded step.
+ */
+ public function get_graded_step() {
+ $step = $this->qa->get_last_step_with_behaviour_var('_try');
+ if ($step->has_behaviour_var('_try')) {
+ return $step;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Determine whether a question state represents an "improvable" result,
+ * that is, whether the user can still improve their score.
+ *
+ * @param question_state $state the question state.
+ * @return bool whether the state is improvable
+ */
+ public function is_state_improvable(question_state $state) {
+ return $state == question_state::$todo;
+ }
}
View
16 question/behaviour/adaptive/renderer.php
@@ -36,13 +36,6 @@
* @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);
@@ -51,7 +44,7 @@ public function controls(question_attempt $qa, question_display_options $options
public function feedback(question_attempt $qa, question_display_options $options) {
// Try to find the last graded step.
- $gradedstep = $this->get_graded_step($qa);
+ $gradedstep = $qa->get_behaviour()->get_graded_step($qa);
if (is_null($gradedstep) || $qa->get_max_mark() == 0 ||
$options->marks < question_display_options::MARK_AND_MAX) {
return '';
@@ -100,14 +93,13 @@ protected function penalty_info(question_attempt $qa, $mark,
}
$output = '';
- // print details of grade adjustment due to penalties
+ // 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()) {
+ // Print information about any new penalty, only relevant if the answer can be improved.
+ if ($qa->get_behaviour()->is_state_improvable($qa->get_state())) {
$output .= ' ' . get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
format_float($qa->get_question()->penalty, $options->markdp));
}
View
309 question/behaviour/adaptive/simpletest/testwalkthrough.php
@@ -39,6 +39,18 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_adaptive_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ protected function get_contains_penalty_info_expectation($penalty) {
+ $penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
+ format_float($penalty, $this->displayoptions->markdp));
+ return new PatternExpectation('/'.preg_quote($penaltyinfo).'/');
+ }
+
+ protected function get_does_not_contain_penalty_info_expectation() {
+ $penaltyinfo = get_string('gradingdetailspenalty', 'qbehaviour_adaptive', 'XXXXX');
+ $penaltypattern = '/'.str_replace('XXXXX', '\\w*', preg_quote($penaltyinfo)).'/';
+ return new NoPatternExpectation($penaltypattern);
+ }
+
public function test_adaptive_multichoice() {
// Create a multiple choice, single response question.
@@ -72,7 +84,8 @@ public function test_adaptive_multichoice() {
$this->get_contains_mc_radio_expectation($wrongindex, true, true),
$this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
$this->get_contains_mc_radio_expectation(($wrongindex + 2) % 3, true, false),
- $this->get_contains_incorrect_expectation());
+ $this->get_contains_incorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33));
$this->assertPattern('/B|C/',
$this->quba->get_response_summary($this->slot));
@@ -102,9 +115,7 @@ public function test_adaptive_multichoice() {
$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(),
- new PatternExpectation('/' . preg_quote(
- get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
- format_float($mc->penalty, $this->displayoptions->markdp))) . '/'));
+ $this->get_does_not_contain_penalty_info_expectation());
$this->assertEqual('A',
$this->quba->get_response_summary($this->slot));
@@ -133,7 +144,8 @@ public function test_adaptive_multichoice() {
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
- $mc->answers[15]->fraction = 1;
+ $mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
+ $mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
$this->quba->regrade_all_questions();
// Verify.
@@ -144,7 +156,7 @@ public function test_adaptive_multichoice() {
$this->get_contains_partcorrect_expectation());
$autogradedstep = $this->get_step($this->get_step_count() - 2);
- $this->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 1, 0.0000001);
}
public function test_adaptive_multichoice2() {
@@ -173,14 +185,16 @@ public function test_adaptive_multichoice2() {
$this->check_current_output(
$this->get_contains_mark_summary(2),
$this->get_contains_submit_button_expectation(true),
- $this->get_contains_correct_expectation());
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation());
- // Save the same correct answer again. Should no do anything.
+ // Save the same correct answer again. Should not do anything.
$numsteps = $this->get_step_count();
$this->process_submission(array('choice0' => 1, 'choice2' => 1));
// Verify.
$this->check_step_count($numsteps);
+ $this->check_current_mark(2);
$this->check_current_state(question_state::$complete);
// Finish the attempt.
@@ -196,6 +210,229 @@ public function test_adaptive_multichoice2() {
$this->get_contains_correct_expectation());
}
+ public function test_adaptive_shortanswer_partially_right() {
+
+ // Create a short answer question
+ $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 a partially correct answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_partcorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit an incorrect answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit a correct answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+ }
+
+ public function test_adaptive_shortanswer_wrong_right_wrong() {
+
+ // Create a short answer question
+ $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 a wrong answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit the same wrong answer again. Nothing should change.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit a correct answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit another incorrect answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+ }
+
+ public function test_adaptive_shortanswer_invalid_after_complete() {
+
+ // Create a short answer question
+ $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 a wrong answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'hippopotamus'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit a correct answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit an empty answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_contains_validation_error_expectation());
+
+ // Submit another wrong answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'bumblebee'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(0.66666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.67),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+ }
+
public function test_adaptive_shortanswer_try_to_submit_blank() {
// Create a short answer question with correct answer true.
@@ -220,6 +457,7 @@ public function test_adaptive_shortanswer_try_to_submit_blank() {
$this->get_contains_marked_out_of_summary(),
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
$this->get_contains_validation_error_expectation());
$this->assertNull($this->quba->get_response_summary($this->slot));
@@ -233,6 +471,7 @@ public function test_adaptive_shortanswer_try_to_submit_blank() {
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_partcorrect_expectation(),
+ $this->get_contains_penalty_info_expectation(0.33),
$this->get_does_not_contain_validation_error_expectation());
// Now submit blank again.
@@ -245,6 +484,60 @@ public function test_adaptive_shortanswer_try_to_submit_blank() {
$this->get_contains_mark_summary(0.8),
$this->get_contains_submit_button_expectation(true),
$this->get_contains_partcorrect_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
$this->get_contains_validation_error_expectation());
}
+
+ public function test_adaptive_numerical() {
+
+ // Create a numerical question
+ $sa = test_question_maker::make_question('numerical', 'pi');
+ $this->start_attempt_at_question($sa, 'adaptive');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit the correct answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => '3.14'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Submit an incorrect answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => '-5'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_penalty_info_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+ }
}
View
7 question/behaviour/adaptivenopenalty/simpletest/testwalkthrough.php
@@ -129,7 +129,8 @@ public function test_multichoice() {
// Now change the correct answer to the question, and regrade.
$mc->answers[13]->fraction = -0.33333333;
- $mc->answers[15]->fraction = 1;
+ $mc->answers[14]->fraction = 1; // We don't know which "wrong" index we chose above!
+ $mc->answers[15]->fraction = 1; // Therefore, treat answers B and C with the same score.
$this->quba->regrade_all_questions();
// Verify.
@@ -139,8 +140,8 @@ public function test_multichoice() {
$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);
+ $autogradedstep = $this->get_step($this->get_step_count() - 3);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 1, 0.0000001);
}
public function test_multichoice2() {
View
15 question/engine/questionattempt.php
@@ -416,6 +416,21 @@ public function get_last_step_with_qt_var($name) {
}
/**
+ * Get the last step with a particular behaviour variable set.
+ * @param string $name the name of the variable to get.
+ * @return question_attempt_step the last step, or a step with no variables
+ * if there was not a real step.
+ */
+ public function get_last_step_with_behaviour_var($name) {
+ foreach ($this->get_reverse_step_iterator() as $step) {
+ if ($step->has_behaviour_var($name)) {
+ return $step;
+ }
+ }
+ return new question_attempt_step_read_only();
+ }
+
+ /**
* Get the latest value of a particular question type variable. That is, get
* the value from the latest step that has it set. Return null if it is not
* set in any step.
View
2  question/type/numerical/question.php
@@ -153,7 +153,7 @@ public function is_same_response(array $prevresponse, array $newresponse) {
$prevresponse, $newresponse, 'unit');
}
- return false;
+ return true;
}
public function get_correct_response() {
Please sign in to comment.
Something went wrong with that request. Please try again.