diff --git a/question/behaviour/adaptive/behaviour.php b/question/behaviour/adaptive/behaviour.php index e85bc6815b0..84bcd0b7c88 100644 --- a/question/behaviour/adaptive/behaviour.php +++ b/question/behaviour/adaptive/behaviour.php @@ -216,4 +216,116 @@ class qbehaviour_adaptive extends question_behaviour_with_save { public function is_state_improvable(question_state $state) { return $state == question_state::$todo; } + + /** + * @return qbehaviour_adaptive_mark_details the information about the current state-of-play, scoring-wise, + * for this adaptive attempt. + */ + public function get_adaptive_marks() { + + // Try to find the last graded step. + $gradedstep = $this->get_graded_step(); + if (is_null($gradedstep) || $this->qa->get_max_mark() == 0) { + // No score yet. + return new qbehaviour_adaptive_mark_details(question_state::$todo); + } + + // Work out the applicable state. + if ($this->qa->get_state()->is_commented()) { + $state = $this->qa->get_state(); + } else { + $state = question_state::graded_state_for_fraction( + $gradedstep->get_behaviour_var('_rawfraction')); + } + + // Prepare the grading details. + $details = $this->adaptive_mark_details_from_step($gradedstep, $state, $this->qa->get_max_mark(), $this->question->penalty); + $details->improvable = $this->is_state_improvable($this->qa->get_state()); + return $details; + } + + /** + * Actually populate the qbehaviour_adaptive_mark_details object. + * @param question_attempt_step $gradedstep the step that holds the relevant mark details. + * @param question_state $state the state corresponding to $gradedstep. + * @param unknown_type $maxmark the maximum mark for this question_attempt. + * @param unknown_type $penalty the penalty for this question, as a fraction. + */ + protected function adaptive_mark_details_from_step(question_attempt_step $gradedstep, + question_state $state, $maxmark, $penalty) { + + $details = new qbehaviour_adaptive_mark_details($state); + $details->maxmark = $maxmark; + $details->actualmark = $gradedstep->get_fraction() * $details->maxmark; + $details->rawmark = $gradedstep->get_behaviour_var('_rawfraction') * $details->maxmark; + + $details->currentpenalty = $penalty * $details->maxmark; + $details->totalpenalty = $details->currentpenalty * $this->qa->get_last_behaviour_var('_try', 0); + + $details->improvable = $this->is_state_improvable($gradedstep->get_state()); + + return $details; + } +} + + +/** + * This class encapsulates all the information about the current state-of-play + * scoring-wise. It is used to communicate between the beahviour and the renderer. + * + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbehaviour_adaptive_mark_details { + /** @var question_state the current state of the question. */ + public $state; + + /** @var float the maximum mark for this question. */ + public $maxmark; + + /** @var float the current mark for this question. */ + public $actualmark; + + /** @var float the raw mark for this question before penalties were applied. */ + public $rawmark; + + /** @var float the the amount of additional penalty this attempt attracted. */ + public $currentpenalty; + + /** @var float the total that will apply to future attempts. */ + public $totalpenalty; + + /** @var bool whether it is possible for this mark to be improved in future. */ + public $improvable; + + /** + * Constructor. + * @param question_state $state + */ + public function __construct($state, $maxmark = null, $actualmark = null, $rawmark = null, + $currentpenalty = null, $totalpenalty = null, $improvable = null) { + $this->state = $state; + $this->maxmark = $maxmark; + $this->actualmark = $actualmark; + $this->rawmark = $rawmark; + $this->currentpenalty = $currentpenalty; + $this->totalpenalty = $totalpenalty; + $this->improvable = $improvable; + } + + /** + * Get the marks, formatted to a certain number of decimal places, in the + * form required by calls like get_string('gradingdetails', 'qbehaviour_adaptive', $a). + * @param int $markdp the number of decimal places required. + * @return array ready to substitute into language strings. + */ + public function get_formatted_marks($markdp) { + return array( + 'max' => format_float($this->maxmark, $markdp), + 'cur' => format_float($this->actualmark, $markdp), + 'raw' => format_float($this->rawmark, $markdp), + 'penalty' => format_float($this->currentpenalty, $markdp), + 'totalpenalty' => format_float($this->totalpenalty, $markdp), + ); + } } diff --git a/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php index 6c415de1836..2dc2e53bc66 100644 --- a/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php +++ b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php @@ -25,8 +25,16 @@ $string['disregardedwithoutpenalty'] = 'The submission was invalid, and has been disregarded without penalty.'; $string['gradingdetails'] = 'Marks for this submission: {$a->raw}/{$a->max}.'; +$string['gradingdetailswithadjustment'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives {$a->cur}/{$a->max}.'; +$string['gradingdetailswithadjustmentpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives {$a->cur}/{$a->max}. This submission attracted a penalty of {$a->penalty}.'; +$string['gradingdetailswithadjustmenttotalpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. Accounting for previous tries, this gives {$a->cur}/{$a->max}. This submission attracted a penalty of {$a->penalty}. Total penalties so far: {$a->totalpenalty}.'; +$string['gradingdetailswithpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. This submission attracted a penalty of {$a->penalty}.'; +$string['gradingdetailswithtotalpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. This submission attracted a penalty of {$a->penalty}. Total penalties so far: {$a->totalpenalty}.'; +$string['notcomplete'] = 'Not complete'; +$string['pluginname'] = 'Adaptive mode'; + +// Old strings these are currently only used in the unit tests, to verify that the new +// strings give the same results as the old strings. $string['gradingdetailsadjustment'] = 'Accounting for previous tries, this gives {$a->cur}/{$a->max}.'; $string['gradingdetailspenalty'] = 'This submission attracted a penalty of {$a}.'; $string['gradingdetailspenaltytotal'] = 'Total penalties so far: {$a}.'; -$string['notcomplete'] = 'Not complete'; -$string['pluginname'] = 'Adaptive mode'; diff --git a/question/behaviour/adaptive/renderer.php b/question/behaviour/adaptive/renderer.php index aa88f9b46c0..3c1f90f8260 100644 --- a/question/behaviour/adaptive/renderer.php +++ b/question/behaviour/adaptive/renderer.php @@ -42,85 +42,70 @@ class qbehaviour_adaptive_renderer extends qbehaviour_renderer { } public function feedback(question_attempt $qa, question_display_options $options) { + + // If the latest answer was invalid, display an informative message. if ($qa->get_state() == question_state::$invalid) { - // If the latest answer was invalid, display an informative message - $output = ''; - $info = $this->disregarded_info(); - if ($info) { - $output = html_writer::tag('div', $info, array('class' => 'gradingdetails')); - } - return $output; + return html_writer::nonempty_tag('div', $this->disregarded_info(), + array('class' => 'gradingdetails')); } - // Try to find the last graded step. + // Otherwise get the details. + return $this->render_adaptive_marks( + $qa->get_behaviour()->get_adaptive_marks(), $options); + } - $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) { + /** + * Display the scoring information about an adaptive attempt. + * @param qbehaviour_adaptive_mark_details contains all the score details we need. + * @param question_display_options $options display options. + */ + public function render_adaptive_marks(qbehaviour_adaptive_mark_details $details, question_display_options $options) { + if ($details->state == question_state::$todo || $options->marks < question_display_options::MARK_AND_MAX) { + // No grades yet. return ''; } - // Display the grading details from the last graded state - $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, $options); - - $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; + // Display the grading details from the last graded state. + $class = $details->state->get_feedback_class(); + return html_writer::tag('div', get_string($class, 'question'), + array('class' => 'correctness ' . $class)) + . html_writer::tag('div', $this->grading_details($details, $options), + array('class' => 'gradingdetails')); } /** * Display the information about the penalty calculations. - * @param question_attempt $qa the question attempt. - * @param object $mark contains information about the current mark. + * @param qbehaviour_adaptive_mark_details contains all the score details we need. * @param question_display_options $options display options. + * @return string html fragment */ - protected function penalty_info(question_attempt $qa, $mark, - question_display_options $options) { + protected function grading_details(qbehaviour_adaptive_mark_details $details, question_display_options $options) { - $currentpenalty = $qa->get_question()->penalty * $qa->get_max_mark(); - $totalpenalty = $currentpenalty * $qa->get_last_behaviour_var('_try', 0); + $mark = $details->get_formatted_marks($options->markdp); - if ($currentpenalty == 0) { - return ''; + if ($details->currentpenalty == 0 && $details->totalpenalty == 0) { + return get_string('gradingdetails', 'qbehaviour_adaptive', $mark); } + $output = ''; // Print details of grade adjustment due to penalties - if ($mark->raw != $mark->cur) { - $output .= ' ' . get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark); - } + if ($details->rawmark != $details->actualmark) { + if (!$details->improvable) { + return get_string('gradingdetailswithadjustment', 'qbehaviour_adaptive', $mark); + } else if ($details->totalpenalty > $details->currentpenalty) { + return get_string('gradingdetailswithadjustmenttotalpenalty', 'qbehaviour_adaptive', $mark); + } else { + return get_string('gradingdetailswithadjustmentpenalty', 'qbehaviour_adaptive', $mark); + } - // 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($currentpenalty, $options->markdp)); - - // Print information about total penalties so far, if larger than current penalty. - if ($totalpenalty > $currentpenalty) { - $output .= ' ' . get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', - format_float($totalpenalty, $options->markdp)); + } else { + if (!$details->improvable) { + return get_string('gradingdetails', 'qbehaviour_adaptive', $mark); + } else if ($details->totalpenalty > $details->currentpenalty) { + return get_string('gradingdetailswithtotalpenalty', 'qbehaviour_adaptive', $mark); + } else { + return get_string('gradingdetailswithpenalty', 'qbehaviour_adaptive', $mark); } } @@ -133,5 +118,4 @@ class qbehaviour_adaptive_renderer extends qbehaviour_renderer { protected function disregarded_info() { return get_string('disregardedwithoutpenalty', 'qbehaviour_adaptive'); } - } diff --git a/question/behaviour/adaptive/tests/mark_display_test.php b/question/behaviour/adaptive/tests/mark_display_test.php new file mode 100644 index 00000000000..f77fdc9ecae --- /dev/null +++ b/question/behaviour/adaptive/tests/mark_display_test.php @@ -0,0 +1,110 @@ +. + + +/** + * This file contains tests that just test the display mark/penalty information. + * + * @package qbehaviour_adaptive + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once(dirname(__FILE__) . '/../../../engine/lib.php'); +require_once(dirname(__FILE__) . '/../behaviour.php'); + + +/** + * Unit tests for the adaptive behaviour the display of mark/penalty information. + * + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbehaviour_adaptive_mark_display_test extends basic_testcase { + /** @var qbehaviour_adaptive_renderer the renderer to test. */ + protected $renderer; + + /** @var question_display_options display options to use when rendering. */ + protected $options; + + protected function setUp() { + global $PAGE; + parent::setUp(); + $this->renderer = $PAGE->get_renderer('qbehaviour_adaptive'); + $this->options = new question_display_options(); + } + + public function test_blank_before_graded() { + $this->assertEquals('', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$todo), $this->options)); + } + + public function test_correct_no_penalty() { + $this->assertEquals('
' . get_string('correct', 'question') . '
' . + '
' . + get_string('gradingdetails', 'qbehaviour_adaptive', + array('cur' => '1.00', 'raw' => '1.00', 'max' => '1.00')) . '
', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$gradedright, 1, 1, 1, 0, 0, false), $this->options)); + } + + public function test_partial_first_try() { + $this->assertEquals('
' . get_string('partiallycorrect', 'question') . '
' . + '
' . + get_string('gradingdetails', 'qbehaviour_adaptive', + array('cur' => '0.50', 'raw' => '0.50', 'max' => '1.00')) . ' ' . + get_string('gradingdetailspenalty', 'qbehaviour_adaptive', '0.10') . '
', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$gradedpartial, 1, 0.5, 0.5, 0.1, 0.1, true), $this->options)); + } + + public function test_partial_second_try() { + $mark = array('cur' => '0.80', 'raw' => '0.90', 'max' => '1.00'); + $this->assertEquals('
' . get_string('partiallycorrect', 'question') . '
' . + '
' . + get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' . + get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . ' ' . + get_string('gradingdetailspenalty', 'qbehaviour_adaptive', '0.10') . ' ' . + get_string('gradingdetailspenaltytotal', 'qbehaviour_adaptive', '0.20') . '
', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$gradedpartial, 1, 0.8, 0.9, 0.1, 0.2, true), $this->options)); + } + + public function test_correct_third_try() { + $mark = array('cur' => '0.80', 'raw' => '1.00', 'max' => '1.00'); + $this->assertEquals('
' . get_string('partiallycorrect', 'question') . '
' . + '
' . + get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' . + get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . '
', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$gradedpartial, 1, 0.8, 1.0, 0.1, 0.3, false), $this->options)); + } + + public function test_correct_third_try_if_we_dont_increase_penalties_for_wrong() { + $mark = array('cur' => '0.80', 'raw' => '1.00', 'max' => '1.00'); + $this->assertEquals('
' . get_string('partiallycorrect', 'question') . '
' . + '
' . + get_string('gradingdetails', 'qbehaviour_adaptive', $mark) . ' ' . + get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark) . '
', + $this->renderer->render_adaptive_marks(new qbehaviour_adaptive_mark_details( + question_state::$gradedpartial, 1, 0.8, 1.0, 0, 0.2, false), $this->options)); + } +} diff --git a/question/behaviour/adaptivenopenalty/renderer.php b/question/behaviour/adaptivenopenalty/renderer.php index 8b1e738cf36..34d3ae184f8 100644 --- a/question/behaviour/adaptivenopenalty/renderer.php +++ b/question/behaviour/adaptivenopenalty/renderer.php @@ -38,8 +38,7 @@ require_once(dirname(__FILE__) . '/../adaptive/renderer.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class qbehaviour_adaptivenopenalty_renderer extends qbehaviour_adaptive_renderer { - protected function penalty_info(question_attempt $qa, $mark, - question_display_options $options) { + protected function penalty_info(qbehaviour_adaptive_mark_details $details, question_display_options $options) { return ''; } protected function disregarded_info() {