From 2e09f40d8b5a0289da4f37f4e1106abdee978d92 Mon Sep 17 00:00:00 2001 From: mkassaei Date: Fri, 20 Dec 2024 12:29:39 +0000 Subject: [PATCH] Tidy up questiontype.php and question.php #834537 --- question.php | 102 ++++--- questiontype.php | 6 - renderer.php | 2 +- tests/backup_and_restore_test.php | 5 - tests/behat/behat_qtype_drawlines.php | 1 + tests/behat/preview.feature | 4 +- tests/helper.php | 2 +- tests/question_test.php | 409 +++++++++++++++++++++----- 8 files changed, 398 insertions(+), 133 deletions(-) diff --git a/question.php b/question.php index 7fb07cd..6eaea40 100644 --- a/question.php +++ b/question.php @@ -59,7 +59,7 @@ class qtype_drawlines_question extends question_graded_automatically { public function get_expected_data() { $expecteddata = []; foreach ($this->lines as $line) { - $expecteddata[$this->choice($line->number - 1)] = PARAM_RAW; + $expecteddata[$this->field($line->number - 1)] = PARAM_RAW; } return $expecteddata; } @@ -75,8 +75,8 @@ public function is_complete_response(array $response): bool { return false; } foreach ($this->lines as $key => $line) { - if (isset($response[$this->choice($key)]) && - !line::are_response_coordinates_valid($response[$this->choice($key)], $line->type)) { + if (isset($response[$this->field($key)]) && + !line::are_response_coordinates_valid($response[$this->field($key)], $line->type)) { return false; } } @@ -87,7 +87,7 @@ public function is_complete_response(array $response): bool { public function get_correct_response() { $response = []; foreach ($this->lines as $key => $line) { - $response[$this->choice($key)] = line::get_coordinates($line->zonestart) . ' ' + $response[$this->field($key)] = line::get_coordinates($line->zonestart) . ' ' . line::get_coordinates($line->zoneend); } return $response; @@ -98,14 +98,14 @@ public function summarise_response(array $response): ?string { $responsewords = []; $answers = []; foreach ($this->lines as $key => $line) { - if (array_key_exists($this->choice($key), $response) && $response[$this->choice($key)] != '') { - $coordinates = explode(' ', $response[$this->choice($key)]); + if (array_key_exists($this->field($key), $response) && $response[$this->field($key)] != '') { + $coordinates = explode(' ', $response[$this->field($key)]); if ($line->type == 'lineinfinite' && count($coordinates) == 4) { - $coordinates = explode(' ', $response[$this->choice($key)]); + $coordinates = explode(' ', $response[$this->field($key)]); $answers[] = 'Line ' . $line->number . ': ' . $coordinates[1] . ' ' . $coordinates[2]; continue; } - $answers[] = 'Line ' . $line->number . ': ' . $response[$this->choice($key)]; + $answers[] = 'Line ' . $line->number . ': ' . $response[$this->field($key)]; } } if (count($answers) > 0) { @@ -115,9 +115,12 @@ public function summarise_response(array $response): ?string { } #[\Override] - public function is_gradable_response(array $response) { + public function is_gradable_response(array $response): bool { + if (!isset($response)) { + return false; + } foreach ($this->lines as $key => $line) { - if (array_key_exists($this->choice($key), $response)) { + if (array_key_exists($this->field($key), $response)) { return true; } } @@ -127,7 +130,7 @@ public function is_gradable_response(array $response) { #[\Override] public function is_same_response(array $prevresponse, array $newresponse) { foreach ($this->lines as $key => $line) { - $fieldname = $this->choice($key); + $fieldname = $this->field($key); if (!question_utils::arrays_same_at_key_missing_is_blank($prevresponse, $newresponse, $fieldname)) { return false; } @@ -135,33 +138,21 @@ public function is_same_response(array $prevresponse, array $newresponse) { return true; } - #[\Override] - public function grade_response(array $response): array { - // Retrieve the number of right responses and the total number of responses. - if ($this->grademethod == 'partial') { - [$numright, $total] = $this->get_num_parts_right_grade_partialt($response); - } else { - [$numright, $total] = $this->get_num_parts_right_grade_allornone($response); - } - $fraction = $numright / $total; - return [$fraction, question_state::graded_state_for_fraction($fraction)]; - } - /** * Get the number of correct choices selected in the response, for 'Give partial credit' grade method. * * @param array $response The response list. * @return array The array of number of correct lines (start, end or both points of lines). */ - public function get_num_parts_right_grade_partialt(array $response): array { + public function get_num_parts_right_grade_partial(array $response): array { if (!$response) { return [0, 0]; } $numpartright = 0; foreach ($this->lines as $key => $line) { - if (array_key_exists($this->choice($key), $response) && $response[$this->choice($key)] !== '') { - $coords = explode(' ', $response[$this->choice($key)]); - if ($line->type == 'lineinfinite') { + if (array_key_exists($this->field($key), $response) && $response[$this->field($key)] !== '') { + $coords = explode(' ', $response[$this->field($key)]); + if ($line->type === line::TYPE_LINE_INFINITE) { if (count($coords) == 2) { // Response with 2 coordinates (x1,y1 x2,y2). if (line::is_item_positioned_correctly_on_axis( @@ -210,9 +201,9 @@ public function get_num_parts_right_grade_allornone(array $response): array { } $numright = 0; foreach ($this->lines as $key => $line) { - if (array_key_exists($this->choice($key), $response) && $response[$this->choice($key)] !== '') { - $coords = explode(' ', $response[$this->choice($key)]); - if ($line->type == 'lineinfinite') { + if (array_key_exists($this->field($key), $response) && $response[$this->field($key)] !== '') { + $coords = explode(' ', $response[$this->field($key)]); + if ($line->type == line::TYPE_LINE_INFINITE) { if (count($coords) == 2) { // Response with 2 coordinates (x1,y1 x2,y2 x3,y3 x4,y4). $isstartrightplace = line::is_item_positioned_correctly_on_axis( @@ -274,7 +265,7 @@ public function get_validation_error(array $response): string { public function classify_response(array $response) { $classifiedresponse = []; foreach ($this->lines as $key => $line) { - if (array_key_exists($this->choice($key), $response) && $response[$this->choice($key)] !== '') { + if (array_key_exists($this->field($key), $response) && $response[$this->field($key)] !== '') { if ($this->grademethod == 'partial') { $fraction = 0.5; } else { @@ -282,7 +273,7 @@ public function classify_response(array $response) { } $classifiedresponse[$key] = new question_classified_response( $line->number, - 'Line ' . $line->number . ': ' . $response[$this->choice($key)], + 'Line ' . $line->number . ': ' . $response[$this->field($key)], $fraction); } else { $classifiedresponse[$key] = question_classified_response::no_response(); @@ -291,6 +282,18 @@ public function classify_response(array $response) { return $classifiedresponse; } + #[\Override] + public function grade_response(array $response): array { + // Retrieve the number of right responses and the total number of responses. + if ($this->grademethod == 'partial') { + [$numright, $numtotal] = $this->get_num_parts_right_grade_partial($response); + } else { + [$numright, $numtotal] = $this->get_num_parts_right_grade_allornone($response); + } + $fraction = $numright / $numtotal; + return [$fraction, question_state::graded_state_for_fraction($fraction)]; + } + /** * Work out a final grade for this attempt, taking into account * all the tries the student made and return the grade value. @@ -302,26 +305,45 @@ public function classify_response(array $response) { * @param int $totaltries The maximum number of tries allowed. * * @return float the fraction that should be awarded for this - * sequence of response. + * sequence of responses. */ - public function compute_final_grade(array $responses, int $totaltries): float { - // TODO: To incorporate the question penalty for interactive with multiple tries behaviour. - + public function compute_final_grade(array $responses, int $totaltries): int|float { + $penalties = 0; $grade = 0; foreach ($responses as $response) { [$fraction, $state] = $this->grade_response($response); - $grade += $fraction; + if ($state->is_graded() === true) { + if ($totaltries === 1) { + return $fraction; + } + $grade = max(0, $fraction - $penalties); + if ($state->get_feedback_class() === 'correct') { + return $grade; + } + if ($state->get_feedback_class() === 'incorrect') { + $penalties += $this->penalty; + } + if ($state->get_feedback_class() === 'partiallycorrect') { + if ($this->grademethod == 'partial') { + [$trynumright, $numtotal] = $this->get_num_parts_right_grade_partial($response); + } else { + [$trynumright, $numtotal] = $this->get_num_parts_right_grade_allornone($response); + } + $partpenaly = (($numtotal - $trynumright) * $this->penalty / $numtotal); + $penalties += min($this->penalty, $partpenaly); + } + } } return $grade; } /** - * Get a choice identifier + * Get a choice index identifier * - * @param int $choice stem number + * @param int $choice * @return string the question-type variable name. */ - public function choice($choice) { + public function field($choice): string { return 'c' . $choice; } } diff --git a/questiontype.php b/questiontype.php index c559065..2ac445d 100644 --- a/questiontype.php +++ b/questiontype.php @@ -172,12 +172,6 @@ public function save_hints($fromform, $withparts = false) { } } - #[\Override] - protected function make_question_instance($questiondata) { - question_bank::load_question_definition_classes($this->name()); - return new qtype_drawlines_question; - } - #[\Override] protected function initialise_question_instance(question_definition $question, $questiondata): void { parent::initialise_question_instance($question, $questiondata); diff --git a/renderer.php b/renderer.php index 41e16b2..5421c6b 100644 --- a/renderer.php +++ b/renderer.php @@ -104,7 +104,7 @@ protected function hidden_field_for_qt_var(question_attempt $qa, $varname, $valu * @return mixed */ protected function hidden_field_choice(question_attempt $qa, $choicenumber, $value = null, $class = null) { - $varname = 'c'. $choicenumber; + $varname = $qa->get_question()->field($choicenumber); $classes = ['choices', 'choice'. $choicenumber]; [, $html] = $this->hidden_field_for_qt_var($qa, $varname, $value, $classes); return $html; diff --git a/tests/backup_and_restore_test.php b/tests/backup_and_restore_test.php index 1f927e9..1a70004 100644 --- a/tests/backup_and_restore_test.php +++ b/tests/backup_and_restore_test.php @@ -83,12 +83,7 @@ public function test_restore_create_qtype_drawlines_mkmap_twolines(): void { WHERE qbe.questioncategoryid = ? AND q.qtype = ?', [$newcategory->id, 'drawlines']); - $newdrawlines->options = $DB->get_record('qtype_drawlines_options', ['questionid' => $newdrawlines->id]); - $newdrawlines->lines = $DB->get_records('qtype_drawlines_lines', ['questionid' => $newdrawlines->id]); - $this->assertSame($newcourseid, $course->id + 1); - $this->assertSame((int)$newdrawlines->id, $drawlines->id + 1); - $this->assertTrue($DB->record_exists('question', ['id' => $newdrawlines->id])); $this->assertTrue($DB->record_exists('qtype_drawlines_options', ['questionid' => $newdrawlines->id])); $this->assertTrue($DB->record_exists('qtype_drawlines_lines', ['questionid' => $newdrawlines->id])); diff --git a/tests/behat/behat_qtype_drawlines.php b/tests/behat/behat_qtype_drawlines.php index 9a71ae1..1987845 100644 --- a/tests/behat/behat_qtype_drawlines.php +++ b/tests/behat/behat_qtype_drawlines.php @@ -15,6 +15,7 @@ // along with Moodle. If not, see . // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + /** * Steps definitions related with the drawlines question type. * diff --git a/tests/behat/preview.feature b/tests/behat/preview.feature index 62bae11..db46e32 100644 --- a/tests/behat/preview.feature +++ b/tests/behat/preview.feature @@ -23,11 +23,11 @@ Feature: Preview a DrawLines question @javascript Scenario: Preview a question using the keyboard - When I am on the "Drawlines to preview" "core_question > preview" page logged in as teacher + Given I am on the "Drawlines to preview" "core_question > preview" page logged in as teacher And I type "up" "360" times on line "1" "line" in the drawlines question And I type "left" "40" times on line "1" "line" in the drawlines question And I type "down" "190" times on line "1" "endcircle" in the drawlines question And I type "left" "200" times on line "1" "endcircle" in the drawlines question - And I press "Submit and finish" + When I press "Submit and finish" Then the state of "Draw 2 lines on the map" question is shown as "Partially correct" And I should see "Mark 0.50 out of 1.00" diff --git a/tests/helper.php b/tests/helper.php index dc97de6..d22a672 100644 --- a/tests/helper.php +++ b/tests/helper.php @@ -218,7 +218,7 @@ public function make_drawlines_question_mkmap_twolines(): qtype_drawlines_questi 11, $question->id, 1, line::TYPE_LINE_SEGMENT, 'Start1', 'Mid1', 'End1', '10,10;12', '300,10;12'), 1 => new line( - 11, $question->id, 2, line::TYPE_LINE_SEGMENT, + 12, $question->id, 2, line::TYPE_LINE_SEGMENT, 'Start2', '', 'End2', '10,200;12', '300,200;12'), ]; diff --git a/tests/question_test.php b/tests/question_test.php index bd3a15d..0f92694 100644 --- a/tests/question_test.php +++ b/tests/question_test.php @@ -21,10 +21,9 @@ use question_state; use qtype_drawlines\line; - defined('MOODLE_INTERNAL') || die(); -global $CFG; +global $CFG; require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/drawlines/tests/helper.php'); require_once($CFG->dirroot . '/question/type/drawlines/question.php'); @@ -40,6 +39,30 @@ */ final class question_test extends \basic_testcase { + /** @var string Line1 right start. */ + const L1_RIGHT_START = '10,10'; + + /** @var string Line1 wrong start. */ + const L1_WRONG_START = '10,123'; + + /** @var string Line1 right end. */ + const L1_RIGHT_END = '300,10'; + + /** @var string Line1 wrong end. */ + const L1_WRONG_END = '300,123'; + + /** @var string Line1 right start. */ + const L2_RIGHT_START = '10,200'; + + /** @var string Line2 wrong start. */ + const L2_WRONG_START = '10,123'; + + /** @var string Line2 right end. */ + const L2_RIGHT_END = '300,200'; + + /** @var string Line2 wrong end. */ + const L2_WRONG_END = '300,123'; + public function test_get_expected_data(): void { $question = \test_question_maker::make_question('drawlines', 'mkmap_twolines'); $question->start_attempt(new question_attempt_step(), 1); @@ -125,107 +148,337 @@ public function test_get_question_summary(): void { $this->assertEquals($expected, $summary); } - public function test_summarise_response(): void { + /** + * Data provider for methods taking question response {@see summarise_response}. + * + * @return array[] + */ + public static function summarise_response_provider(): array { + $l1rightstart = self::L1_RIGHT_START; + $l1wrongstart = self::L1_WRONG_START; + $l1rightend = self::L1_RIGHT_END; + $l1wrongend = self::L1_WRONG_END; + + $l2rightstart = self::L2_RIGHT_START; + $l2wrongstart = self::L2_WRONG_START; + $l2rightend = self::L2_RIGHT_END; + $l2wrongend = self::L2_WRONG_END; + + return [ + 'L1=00 L2=00' => ['Line 1: ' . "$l1wrongstart $l1wrongend". ', Line 2: ' . "$l2wrongstart $l2wrongend", + ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"]], + 'L1=10 L2=00' => ['Line 1: ' . "$l1rightstart $l1wrongend". ', Line 2: ' . "$l2wrongstart $l2wrongend", + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"]], + 'L1=11 L2=00' => ['Line 1: ' . "$l1rightstart $l1rightend". ', Line 2: ' . "$l2wrongstart $l2wrongend", + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"]], + 'L1=10 L2=10' => ['Line 1: ' . "$l1rightstart $l1wrongend". ', Line 2: ' . "$l2rightstart $l2wrongend", + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2wrongend"]], + 'L1=10 L2=01' => ['Line 1: ' . "$l1rightstart $l1wrongend". ', Line 2: ' . "$l2wrongstart $l2rightend", + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2rightend"]], + 'L1=11 L2=10' => ['Line 1: ' . "$l1rightstart $l1rightend". ', Line 2: ' . "$l2rightstart $l2wrongend", + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"]], + 'L1=11 L2=11' => ['Line 1: ' . "$l1rightstart $l1rightend". ', Line 2: ' . "$l2rightstart $l2rightend", + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"]], + ]; + } + + /** + * Test the summarise_response function. + * + * @dataProvider summarise_response_provider + * @param int|float $expected + * @param array $responses + * @return void + */ + public function test_summarise_response(string $expected, array $response): void { $question = \test_question_maker::make_question('drawlines', 'mkmap_twolines'); $question->start_attempt(new question_attempt_step(), 1); - // Correct responses with full mark for both Lines (mark = 1). - $correctresponse = $question->get_correct_response(); - $expected = 'Line 1: 10,10 300,10, Line 2: 10,200 300,200'; - $actual = $question->summarise_response($correctresponse); - $this->assertEquals($expected, $actual); - - // Partially correct responses with full marks for Line 1 and half of mark for Line 2 (mark = 0.75). - $expected = 'Line 1: 10,10 300,10, Line 2: 10,200 300,123'; - $actual = $question->summarise_response(['c0' => '10,10 300,10', 'c1' => '10,200 300,123']); + $actual = $question->summarise_response($response); $this->assertEquals($expected, $actual); + } - // Partially correct responses with full marks for Line 1 and no mark for Line 2 (mark = 0.5). - $expected = 'Line 1: 10,10 300,10, Line 2: 10,123 300,123'; - $actual = $question->summarise_response(['c0' => '10,10 300,10', 'c1' => '10,123 300,123']); - $this->assertEquals($expected, $actual); + /** + * Data provider for methods taking question response {@see get_num_part_right_...}. + * + * @return array[] + */ + public static function response_provider(): array { + $l1rightstart = self::L1_RIGHT_START; + $l1wrongstart = self::L1_WRONG_START; + $l1rightend = self::L1_RIGHT_END; + $l1wrongend = self::L1_WRONG_END; + + $l2rightstart = self::L2_RIGHT_START; + $l2wrongstart = self::L2_WRONG_START; + $l2rightend = self::L2_RIGHT_END; + $l2wrongend = self::L2_WRONG_END; + + return [ + 'part L1=00 L2=00' => [0, ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], 'partial'], + 'part L1=10 L2=00' => [1, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], 'partial'], + 'part L1=11 L2=00' => [2, ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"], 'partial'], + 'part L1=10 L2=10' => [2, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2wrongend"], 'partial'], + 'part L1=10 L2=01' => [2, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2rightend"], 'partial'], + 'part L1=11 L2=10' => [3, ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], 'partial'], + 'part L1=11 L2=11' => [4, ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], 'partial'], + + 'all L1=00 L2=00' => [0, ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], 'allnone'], + 'all L1=10 L2=00' => [0, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], 'allnone'], + 'all L1=10 L2=10' => [0, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2wrongend"], 'allnone'], + 'all L1=10 L2=01' => [0, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2rightend"], 'allnone'], + 'all L1=10 L2=11' => [1, ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2rightend"], 'allnone'], + 'all L1=11 L2=10' => [1, ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], 'allnone'], + 'all L1=11 L2=11' => [2, ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], 'allnone'], + ]; } - public function test_get_num_parts_right_grade_partialt(): void { + /** + * Test the get_num_parts_right_grade_partial function. + * + * @dataProvider response_provider + * @param int|float $expected + * @param array $responses + * @param string $grademethod + * @return void + */ + public function test_get_num_parts_right_grade_partial(int $expected, array $response, string $grademethod): void { + if ($grademethod !== 'partial') { + return; + } + $question = \test_question_maker::make_question('drawlines'); $question->start_attempt(new question_attempt_step(), 1); - $correctresponse = $question->get_correct_response(); - [$numpartright, $total] = $question->get_num_parts_right_grade_partialt($correctresponse); - $this->assertEquals(4, $numpartright); - $this->assertEquals(4, $total); - - $response = ['c0' => '10,10 300,123', 'c1' => '10,123 300,123']; - [$numpartright, $total] = $question->get_num_parts_right_grade_partialt($response); - $this->assertEquals(1, $numpartright); - $this->assertEquals(4, $total); - - $response = ['c0' => '10,10 300,10', 'c1' => '10,123 300,123']; - [$numpartright, $total] = $question->get_num_parts_right_grade_partialt($response); - $this->assertEquals(2, $numpartright); - $this->assertEquals(4, $total); - - $response = ['c0' => '10,10 300,10', 'c1' => '10,200 300,123']; - [$numpartright, $total] = $question->get_num_parts_right_grade_partialt($response); - $this->assertEquals(3, $numpartright); + [$numpartright, $total] = $question->get_num_parts_right_grade_partial($response); + $this->assertEquals($expected, $numpartright); $this->assertEquals(4, $total); } - public function test_get_num_parts_right_grade_allornone(): void { + /** + * Test the get_num_parts_right_grade_allornone function. + * + * @dataProvider response_provider + * @param int|float $expected + * @param array $responses + * @param string $grademethod + * @return void + */ + public function test_get_num_parts_right_grade_allornone(int|float $expected, array $response, string $grademethod): void { + if ($grademethod !== 'allnone') { + return; + } + $question = \test_question_maker::make_question('drawlines'); $question->start_attempt(new question_attempt_step(), 1); - $correctresponse = $question->get_correct_response(); - [$numright, $total] = $question->get_num_parts_right_grade_allornone($correctresponse); - $this->assertEquals(2, $numright); - $this->assertEquals(2, $total); - - $response = ['c0' => '10,10 300,123', 'c1' => '10,123 300,123']; [$numright, $total] = $question->get_num_parts_right_grade_allornone($response); - $this->assertEquals(0, $numright); - $this->assertEquals(2, $total); - - $response = ['c0' => '10,10 300,10', 'c1' => '10,123 300,123']; - [$numright, $total] = $question->get_num_parts_right_grade_allornone($response); - $this->assertEquals(1, $numright); + $this->assertEquals($expected, $numright); $this->assertEquals(2, $total); + } - $response = ['c0' => '10,10 300,10', 'c1' => '10,200 300,123']; - [$numright, $total] = $question->get_num_parts_right_grade_allornone($response); - $this->assertEquals(1, $numright); - $this->assertEquals(2, $total); + /** + * Data provider for methods taking question response {@see grade_response..., ...}. + * + * @return array[] + */ + public static function grade_response_provider(): array { + $l1rightstart = self::L1_RIGHT_START; + $l1wrongstart = self::L1_WRONG_START; + $l1rightend = self::L1_RIGHT_END; + $l1wrongend = self::L1_WRONG_END; + + $l2rightstart = self::L2_RIGHT_START; + $l2wrongstart = self::L2_WRONG_START; + $l2rightend = self::L2_RIGHT_END; + $l2wrongend = self::L2_WRONG_END; + + return [ + 'part L1=00 L2=00' => [ + [0, question_state::$gradedwrong], + ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + 'partial', + ], + 'part L1=10 L2=00' => [ + [0.25, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + 'partial', + ], + 'part L1=11 L2=00' => [ + [0.50, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"], + 'partial', + ], + 'part L1=10 L2=10' => [ + [0.50, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2wrongend"], + 'partial', + ], + 'part L1=10 L2=01' => [ + [0.50, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2rightend"], + 'partial', + ], + 'part L1=11 L2=10' => [ + [0.75, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], + 'partial', + ], + 'part L1=11 L2=11' => [ + [1, question_state::$gradedright], + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + 'partial', + ], + + 'all L1=00 L2=00' => [ + [0, question_state::$gradedwrong], + ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + 'allnone', + ], + 'all L1=10 L2=00' => [ + [0, question_state::$gradedwrong], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + 'allnone', + ], + 'all L1=11 L2=00' => [ + [0, question_state::$gradedwrong], + ['c0' => "$l1wrongstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"], + 'allnone', + ], + 'all L1=10 L2=10' => [ + [0, question_state::$gradedwrong], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2wrongend"], + 'allnone', + ], + 'all L1=10 L2=11' => [ + [0.50, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2rightstart $l2rightend"], + 'allnone', + ], + 'all L1=11 L2=10' => [ + [0.50, question_state::$gradedpartial], + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], + 'allnone', + ], + 'all L1=11 L2=11' => [ + [1, question_state::$gradedright], + ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + 'allnone', + ], + ]; } - public function test_compute_final_grade(): void { + /** + * Test the grade_response function. + * + * @dataProvider grade_response_provider + * @param array $expected A list of fraction an state (graderight, gradewrong, gradepartial). + * @param array $responses + * @param string $grademethod + * @return void + */ + public function test_grade_response(array $expected, array $response, string $grademethod): void { $question = \test_question_maker::make_question('drawlines'); + $question->grademethod = $grademethod; $question->start_attempt(new question_attempt_step(), 1); - // TODO: To incorporate the question penalty for interactive with multiple tries behaviour. - - $totaltries = 1; - - $response = ['c0' => '100,10 300,100', 'c1' => '10,123 300,123']; - $responses[] = $response; - $fraction = $question->compute_final_grade($responses, $totaltries); - $this->assertEquals($fraction, 0 / $totaltries, 'Incorrect responses should return fraction of 0'); - - $responses = null; - $response = ['c0' => '10,10 300,10', 'c1' => '10,123 300,123']; - $responses[] = $response; - $fraction = $question->compute_final_grade($responses, $totaltries); - $this->assertEquals($fraction, 0.5 / $totaltries, - 'Partially correct responses(line 1 is correct and line 2 is incorrect) should return fraction of 0.5'); + $this->assertEquals($expected, $question->grade_response($response)); + } - $responses = null; - $response = ['c0' => '10,10 300,10', 'c1' => '10,200 300,123']; - $responses[] = $response; - $fraction = $question->compute_final_grade($responses, $totaltries); - $this->assertEquals($fraction, 0.75 / $totaltries, - 'Partially correct responses(line 1 is correct and line 2 is half-correct) should return fraction of 0.75'); + /** + * Data provider for {@see test_compute_final_grade}. + * + * @return array[] + */ + public static function compute_final_grade_provider(): array { + $l1rightstart = self::L1_RIGHT_START; + $l1wrongstart = self::L1_WRONG_START; + $l1rightend = self::L1_RIGHT_END; + $l1wrongend = self::L1_WRONG_END; + + $l2rightstart = self::L2_RIGHT_START; + $l2wrongstart = self::L2_WRONG_START; + $l2rightend = self::L2_RIGHT_END; + $l2wrongend = self::L2_WRONG_END; + + return [ + // Single try. + 'L1=00 L2=00' => [0, ['1' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"]], 1], + 'L1=11 L2=00' => [0.50, ['1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"]], 1], + 'L1=01 L2=10' => [0.50, ['1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"]], 1], + 'L1=11 L2=01' => [0.75, ['1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2rightend"]], 1], + 'L1=11 L2=10' => [0.75, ['1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"]], 1], + 'L1=11 L2=11' => [1, ['1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"]], 1], + + // Multiple tries with penalties, totaltries set to 3. + 'T1: L1=00 L2=00, T2: L1=00 L2=00, T3: L1=11 L2=11' => [0.33334, [ + '1' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '3' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=00 L2=00, T2: L1=10 L2=00, T3: L1=11 L2=11' => [0.41667, [ + '1' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '3' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=00 L2=00, T2: L1=11 L2=00, T3: L1=11 L2=11' => [0.5, [ + '1' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2wrongend"], + '3' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=10 L2=00, T2: L1=11 L2=10, T3: L1=11 L2=11' => [0.66667, [ + '1' => ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], + '3' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=00 L2=00, T2: L1=11 L2=11:' => [0.66667, [ + '1' => ['c0' => "$l1wrongstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=10 L2=00, T2: L1=11 L2=11:' => [0.75, [ + '1' => ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=11 L2=00, T2: L1=11 L2=11:' => [0.916667, [ + '1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2wrongstart $l2rightend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=11 L2=11:' => [1, [ + '1' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2rightend"], + ], 3, + ], + 'T1: L1=10 L2=00, T2: L1=11 L2=10, T3: L1=11 L2=10' => [0.41667, [ + '1' => ['c0' => "$l1rightstart $l1wrongend", 'c1' => "$l2wrongstart $l2wrongend"], + '2' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], + '3' => ['c0' => "$l1rightstart $l1rightend", 'c1' => "$l2rightstart $l2wrongend"], + ], 3, + ], + ]; + } - $responses = null; - $correctresponse = $question->get_correct_response(); - $responses[] = $correctresponse; + /** + * Test the compute_final_grade function. + * + * @dataProvider compute_final_grade_provider + * @param int|float $expected + * @param array $responses + * @param int $totaltries + * @return void + */ + public function test_compute_final_grade($expected, $responses, $totaltries): void { + $question = \test_question_maker::make_question('drawlines'); + $question->start_attempt(new question_attempt_step(), 1); $fraction = $question->compute_final_grade($responses, $totaltries); - $this->assertEquals($fraction, 1 / $totaltries, 'All correct responses should return fraction of 1'); + if (is_float($fraction)) { + $this->assertEqualsWithDelta($expected, $fraction, 0.00001); + } else { + $this->assertEquals($expected, $fraction); + } } }