diff --git a/.upgradenotes/MDL-86223-2025081919563565.yml b/.upgradenotes/MDL-86223-2025081919563565.yml new file mode 100644 index 00000000000..dc47273c490 --- /dev/null +++ b/.upgradenotes/MDL-86223-2025081919563565.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-86223 +notes: + mod_quiz: + - message: >- + Add a groupidlist option to quiz_num_attempt_summary, quiz_num_attempts and quiz_num_users_who_can_attempt + to filter those number by groups (the new argument is a list of ids for groups) + type: improved diff --git a/public/mod/quiz/classes/courseformat/overview.php b/public/mod/quiz/classes/courseformat/overview.php index 309d9c6a204..4eeb0ea23e0 100644 --- a/public/mod/quiz/classes/courseformat/overview.php +++ b/public/mod/quiz/classes/courseformat/overview.php @@ -123,8 +123,9 @@ class overview extends \core_courseformat\activityoverviewbase { if (!has_capability('mod/quiz:viewreports', $this->cm->context)) { return null; } - $numstudentattempted = quiz_num_users_who_attempted($this->cm); - $numstudentwhocanattempt = quiz_num_users_who_can_attempt($this->cm); + $groups = array_map(fn($group) => $group->id, $this->get_groups_for_filtering()); + $numstudentattempted = quiz_num_users_who_attempted($this->cm, $groups); + $numstudentwhocanattempt = quiz_num_users_who_can_attempt($this->cm, $groups); $studentattemptedvalue = get_string( 'count_of_total', 'core', @@ -148,10 +149,11 @@ class overview extends \core_courseformat\activityoverviewbase { if (!has_capability('mod/quiz:viewreports', $this->cm->context)) { return null; } - $numattempts = quiz_num_attempts($this->cm); + $groups = array_map(fn($group) => $group->id, $this->get_groups_for_filtering()); + $total = quiz_num_attempts($this->cm, $groups); $overviewdialog = new overviewdialog( - buttoncontent: $numattempts->total, + buttoncontent: $total, description: get_string('totalattempts', 'mod_quiz'), definition: ['buttonclasses' => button::SECONDARY_OUTLINE->classes() . ' dropdown-toggle'], ); @@ -165,8 +167,8 @@ class overview extends \core_courseformat\activityoverviewbase { $allowedattempts, ); - $numstudentattempted = quiz_num_users_who_attempted($this->cm); - $numstudentwhocanattempt = quiz_num_users_who_can_attempt($this->cm); + $numstudentattempted = quiz_num_users_who_attempted($this->cm, $groups); + $numstudentwhocanattempt = quiz_num_users_who_can_attempt($this->cm, $groups); if ($numstudentwhocanattempt > 0 && $numstudentattempted > 0) { $overviewdialog->add_item( get_string('averageattemptsperstudent', 'mod_quiz'), @@ -176,7 +178,7 @@ class overview extends \core_courseformat\activityoverviewbase { return new overviewitem( name: get_string('totalattempts', 'mod_quiz'), - value: $numattempts->total, + value: $total, content: $overviewdialog, textalign: text_align::START, ); diff --git a/public/mod/quiz/lib.php b/public/mod/quiz/lib.php index 6284e32f7ba..12cfc239f02 100644 --- a/public/mod/quiz/lib.php +++ b/public/mod/quiz/lib.php @@ -1570,53 +1570,59 @@ function quiz_reset_userdata($data) { function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) { global $USER; [$course, $fullcminfo] = get_course_and_cm_from_instance($quiz, 'quiz'); - $numattempts = quiz_num_attempts($fullcminfo, $currentgroup); - if (!$numattempts->total && !$returnzero) { + $totalattempts = quiz_num_attempts($fullcminfo); + if (!$totalattempts && !$returnzero) { return ''; } if (groups_get_activity_groupmode($fullcminfo)) { if ($currentgroup) { - return get_string('attemptsnumthisgroup', 'quiz', $numattempts); - } else if (groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { - return get_string('attemptsnumyourgroups', 'quiz', $numattempts); + $groupattempts = quiz_num_attempts($fullcminfo, [$currentgroup]); + return get_string( + 'attemptsnumthisgroup', + 'quiz', + [ + 'total' => $totalattempts, + 'group' => $groupattempts, + ] + ); + } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { + $groupattempts = quiz_num_attempts($fullcminfo, array_keys($groups)); + return get_string( + 'attemptsnumyourgroups', + 'quiz', + [ + 'total' => $totalattempts, + 'group' => $groupattempts, + ] + ); } } - return get_string('attemptsnum', 'quiz', $numattempts->total); + return get_string('attemptsnum', 'quiz', $totalattempts); } /** * Return a numerical summary of the number of attempts that have been made at a particular quiz. * * @param cm_info $cm - * @param int $currentgroup - * @return stdClass with the number of attempts in the 'total' field and the number of attempts from the group in the 'group' field. + * @param array $groupidlist array of group ids to count attempts from, if array is empty we return all records from all groups. + * @return int the number of attempts filtered by group if groupidlist is not empty (or all attempts). */ -function quiz_num_attempts(cm_info $cm, int $currentgroup = 0): stdClass { - global $DB, $USER; - $numattempts = new stdClass(); - $numattempts->total = $DB->count_records('quiz_attempts', ['quiz' => $cm->instance, 'preview' => 0]); - if ($numattempts->total) { - if ($currentgroup) { - $numattempts->group = $DB->count_records_sql( - 'SELECT COUNT(DISTINCT qa.id) - FROM {quiz_attempts} qa - JOIN {groups_members} gm ON qa.userid = gm.userid - WHERE quiz = ? AND preview = 0 AND groupid = ?', - [$cm->instance, $currentgroup], - ); - } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { - [$usql, $params] = $DB->get_in_or_equal(array_keys($groups)); - $numattempts->group = $DB->count_records_sql( - 'SELECT COUNT(DISTINCT qa.id) - FROM {quiz_attempts} qa - JOIN {groups_members} gm ON qa.userid = gm.userid - WHERE quiz = ? AND preview = 0 AND groupid ' . $usql, - array_merge([$cm->instance], $params) - ); - } - } - return $numattempts; +function quiz_num_attempts(cm_info $cm, array $groupidlist = []): int { + global $DB; + $context = context_module::instance($cm->id); + $groupjoins = groups_get_members_join($groupidlist, 'qa.userid', $context); + $groupjoins->wheres = !empty($groupjoins->wheres) ? "AND $groupjoins->wheres" : ''; + $params = array_merge(['quizid' => $cm->instance], $groupjoins->params ?? []); + + return $DB->count_records_sql( + "SELECT COUNT(DISTINCT qa.id) + FROM {quiz_attempts} qa + {$groupjoins->joins} + WHERE quiz = :quizid + AND preview = 0 {$groupjoins->wheres}", + $params + ); } /** @@ -1624,20 +1630,35 @@ function quiz_num_attempts(cm_info $cm, int $currentgroup = 0): stdClass { * Returns 0 if no attempts have been made yet. * * @param cm_info $cm + * @param array $groupidlist array of group ids to count attempts from, if array is empty we return all records from all groups. * @return int */ -function quiz_num_users_who_attempted(cm_info $cm): int { +function quiz_num_users_who_attempted(cm_info $cm, array $groupidlist = []): int { global $DB; $context = context_module::instance($cm->id); + $studentsjoins = get_enrolled_with_capabilities_join($context, '', ['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']); - $params = array_merge(['quiz' => $cm->instance, 'preview' => 0], $studentsjoins->params); + $studentsjoins->wheres = empty($studentsjoins->wheres) ? '1=1' : $studentsjoins->wheres; + + $groupjoins = groups_get_members_join($groupidlist, 'u.id', $context); + $groupjoins->wheres = !empty($groupjoins->wheres) ? "AND $groupjoins->wheres" : ''; + + $params = array_merge( + ['quiz' => $cm->instance, 'preview' => 0], + $studentsjoins->params ?? [], + $groupjoins->params ?? [] + ); + return $DB->count_records_sql( "SELECT COUNT(DISTINCT u.id) - FROM {quiz_attempts} qa - LEFT JOIN {user} u ON qa.userid = u.id - $studentsjoins->joins - WHERE $studentsjoins->wheres AND qa.quiz = :quiz AND qa.preview = :preview", - $params, + FROM {quiz_attempts} qa + LEFT JOIN {user} u ON qa.userid = u.id + {$studentsjoins->joins} + {$groupjoins->joins} + WHERE qa.quiz = :quiz + AND qa.preview = :preview + AND {$studentsjoins->wheres} {$groupjoins->wheres}", + $params ); } @@ -1645,22 +1666,33 @@ function quiz_num_users_who_attempted(cm_info $cm): int { * Return a number of users who can attempt a particular quiz, * * @param cm_info $cm + * @param array $groupidlist array of group ids to count attempts from, if array is empty we return all records from all groups. * @return int */ -function quiz_num_users_who_can_attempt(cm_info $cm): int { +function quiz_num_users_who_can_attempt(cm_info $cm, array $groupidlist = []): int { global $DB; - // Get the list of students who can attempt this quiz. $context = context_module::instance($cm->id); + $studentsjoins = get_enrolled_with_capabilities_join($context, '', ['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']); + $studentsjoins->wheres = empty($studentsjoins->wheres) ? '1=1' : $studentsjoins->wheres; + + $groupjoins = groups_get_members_join($groupidlist, 'u.id', $context); + $groupjoins->wheres = !empty($groupjoins->wheres) ? "AND $groupjoins->wheres" : ''; + + $params = array_merge( + $studentsjoins->params, + $groupjoins->params + ); + return $DB->count_records_sql( "SELECT COUNT(DISTINCT u.id) - FROM {user} u - $studentsjoins->joins - WHERE $studentsjoins->wheres", - $studentsjoins->params, + FROM {user} u + {$studentsjoins->joins} + {$groupjoins->joins} + WHERE {$studentsjoins->wheres} {$groupjoins->wheres}", + $params ); } - /** * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link * to the quiz reports. diff --git a/public/mod/quiz/tests/courseformat/overview_test.php b/public/mod/quiz/tests/courseformat/overview_test.php index 44628c2e269..07cefc1911a 100644 --- a/public/mod/quiz/tests/courseformat/overview_test.php +++ b/public/mod/quiz/tests/courseformat/overview_test.php @@ -96,14 +96,7 @@ final class overview_test extends \advanced_testcase { $this->resetAfterTest(); $this->setAdminUser(); - ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups( - [ - 's1' => ['student', 'g1', 2], - 's2' => ['student', null, 1], - 't1' => ['editingteacher', null, null], - 't2' => ['teacher', 'g1', null], - ] - ); + ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups([]); $this->setUser($users[$currentuser]); $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); $item = overviewfactory::create($cminfo)->get_actions_overview(); @@ -130,57 +123,60 @@ final class overview_test extends \advanced_testcase { 'currentuser' => 's1', 'expected' => null, ], - 'Teacher' => [ + 'Editing Teacher' => [ 'currentuser' => 't1', 'expected' => [ 'name' => get_string('actions'), 'value' => '', ], ], + 'Non editing Teacher' => [ + 'currentuser' => 't2', + 'expected' => [ + 'name' => get_string('actions'), + 'value' => '', + ], + ], ]; } /** * Test get_total_attempts_overview. * - * @param string $currentuser - * @param string|null $expected + * @param int $groupmode + * @param array $expected * @return void * @dataProvider provider_test_get_total_attempts_overview */ public function test_get_extra_totalattempts_overview( - string $currentuser, - ?string $expected + int $groupmode, + array $expected, ): void { $this->resetAfterTest(); $this->setAdminUser(); - ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups( - [ - 's1' => ['student', 'g1', 2], - 's2' => ['student', null, 1], - 't1' => ['editingteacher', null, null], - 't2' => ['teacher', 'g1', null], - ] - ); - $this->setUser($users[$currentuser]); - $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); - $overview = overviewfactory::create($cminfo); + ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups([], $groupmode); + foreach ($expected as $currentuser => $studentswhoattempted) { + $this->setUser($users[$currentuser]); + $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); + $overview = overviewfactory::create($cminfo); - $reflection = new \ReflectionClass($overview); - $method = $reflection->getMethod('get_extra_total_attempts_overview'); - $method->setAccessible(true); - $item = $method->invoke($overview); + $reflection = new \ReflectionClass($overview); + $method = $reflection->getMethod('get_extra_total_attempts_overview'); + $method->setAccessible(true); + $item = $method->invoke($overview); - if ($expected === null) { - $this->assertNull($item); - return; + if ($studentswhoattempted === null) { + $this->assertNull($item, 'Expected null for user: ' . $currentuser); + continue; + } + + $this->assertEquals( + $studentswhoattempted, + $item->get_value(), + "Failed for user: $currentuser" + ); } - - $this->assertEquals( - $expected, - $item->get_value() - ); } /** @@ -190,13 +186,29 @@ final class overview_test extends \advanced_testcase { */ public static function provider_test_get_total_attempts_overview(): array { return [ - 'Teacher t1' => [ - 'currentuser' => 't1', - 'expected' => "3", + 'Without groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + 't1' => 5, // Count all attempts (even teacher's attempts). + 't2' => 5, + 's1' => null, + ], ], - 'Student' => [ - 'currentuser' => 's1', - 'expected' => null, + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + 't1' => 5, + 't2' => 3, // User 1 two attempts, teacher 2 one attempt (counted). + 's1' => null, + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + 't1' => 5, + 't2' => 5, + 's1' => null, + ], ], ]; } @@ -204,43 +216,38 @@ final class overview_test extends \advanced_testcase { /** * Test get_students_who_attempted_overview. * - * @param string $currentuser - * @param ?string $expected + * @param int $groupmode + * @param array $expected * @return void * @dataProvider provider_test_get_students_who_attempted_overview **/ - public function test_get_extra_attemptedstudents_overview( - string $currentuser, - ?string $expected + public function test_get_extra_studentswhoattempted_overview( + int $groupmode, + array $expected, ): void { $this->resetAfterTest(); $this->setAdminUser(); + ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups([], $groupmode); + foreach ($expected as $currentuser => $totalattempts) { + $this->setUser($users[$currentuser]); + $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); + $overview = overviewfactory::create($cminfo); - ['users' => $users, 'cm' => $cm] = $this->setup_users_course_groups( - [ - 's1' => ['student', 'g1', 2], - 's2' => ['student', null, 1], - 't1' => ['editingteacher', null, null], - 't2' => ['teacher', 'g1', null], - ] - ); - $this->setUser($users[$currentuser]); - $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); - $overview = overviewfactory::create($cminfo); + $reflection = new \ReflectionClass($overview); + $method = $reflection->getMethod('get_extra_students_who_attempted_overview'); + $method->setAccessible(true); + $item = $method->invoke($overview); - $reflection = new \ReflectionClass($overview); - $method = $reflection->getMethod('get_extra_students_who_attempted_overview'); - $method->setAccessible(true); - $item = $method->invoke($overview); - - if ($expected === null) { - $this->assertNull($item); - return; + if ($totalattempts === null) { + $this->assertNull($item, 'Expected null for user: ' . $currentuser); + return; + } + $this->assertEquals( + $totalattempts, + $item->get_value(), + "Failed for user: $currentuser" + ); } - $this->assertEquals( - $expected, - $item->get_value() - ); } /** @@ -250,13 +257,29 @@ final class overview_test extends \advanced_testcase { */ public static function provider_test_get_students_who_attempted_overview(): array { return [ - 'Teacher' => [ - 'currentuser' => 't1', - 'expected' => "2 of 2", + 'With no groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + 't1' => "3 of 4", // 3 students and one teacher out (not counted) of 4 made at least one attempt. + 't2' => "3 of 4", + 's1' => null, + ], ], - 'Student' => [ - 'currentuser' => 's1', - 'expected' => null, + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + 't1' => "3 of 4", // Teacher 1 can see all groups. + 't2' => "1 of 1", // Only student 1 in group 1 made at least one attempt. + 's1' => null, + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + 't1' => "3 of 4", // 3 students and one teacher out (not counted) of 4 made at least one attempt. + 't2' => "3 of 4", + 's1' => null, + ], ], ]; } @@ -265,13 +288,24 @@ final class overview_test extends \advanced_testcase { * Set up users, course, groups and quiz for testing. * * @param array $data Array of user data with username as key and an array of role, group and attempts number as value. + * @param int $groupmode The group mode to use for the course. * @return array An array containing users, groups, quiz, course module and attempts. */ - private function setup_users_course_groups(array $data): array { + private function setup_users_course_groups(array $data, int $groupmode = SEPARATEGROUPS): array { $generator = $this->getDataGenerator(); - + if (empty($data)) { + $data = [ + 's1' => ['student', 'g1', 2], + 's2' => ['student', null, 1], + 's3' => ['student', 'g2', 1], + 's4' => ['student', 'g2', 0], + 't1' => ['editingteacher', null, null], + 't2' => ['teacher', 'g1', 1], + // Teachers without groups are tested in the overview tests (as they produce a row with an error message). + ]; + } // Create a course and a quiz. - $course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => SEPARATEGROUPS]); + $course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => $groupmode]); $quiz = $generator->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]); $cm = get_coursemodule_from_instance('quiz', $quiz->id); diff --git a/public/mod/quiz/tests/lib_test.php b/public/mod/quiz/tests/lib_test.php index fef2829a9c9..a301c50ff35 100644 --- a/public/mod/quiz/tests/lib_test.php +++ b/public/mod/quiz/tests/lib_test.php @@ -422,7 +422,7 @@ final class lib_test extends \advanced_testcase { $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $now = 100; - $override1 = (object)[ + $override1 = (object) [ 'quiz' => $quiz->id, 'groupid' => $group1->id, 'timeopen' => $now, @@ -430,7 +430,7 @@ final class lib_test extends \advanced_testcase { ]; $DB->insert_record('quiz_overrides', $override1); - $override2 = (object)[ + $override2 = (object) [ 'quiz' => $quiz->id, 'groupid' => $group2->id, 'timeopen' => $now - 10, @@ -877,7 +877,7 @@ final class lib_test extends \advanced_testcase { private function create_action_event($courseid, $instanceid, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; - $event->modulename = 'quiz'; + $event->modulename = 'quiz'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; @@ -1032,9 +1032,13 @@ final class lib_test extends \advanced_testcase { /** * Test the quiz_num_attempt_summary function. + * + * @param int $groupmode The group mode to use for the test. + * @param array $expected The expected results for each user. * @covers ::quiz_num_attempt_summary + * @dataProvider num_attempts_data_provider */ - public function test_quiz_num_attempt_summary(): void { + public function test_quiz_num_attempt_summary(int $groupmode, array $expected): void { $this->resetAfterTest(); $this->setAdminUser(); [ @@ -1042,28 +1046,120 @@ final class lib_test extends \advanced_testcase { 'groups' => $groups, 'quiz' => $quiz, 'cm' => $cm, - ] = $this->setup_users_course_groups( - [ - 'user1' => ['student', 'g1', 2], - 'user2' => ['student', null, 1], - 'teacher1' => ['editingteacher', null, null], - 'teacher2' => ['teacher', 'g1', null], - ] - ); - // Check the summary. - $this->setUser($users['teacher1']); - $this->assertEquals('Attempts: 3', quiz_num_attempt_summary($quiz, $cm)); - $this->assertEquals('Attempts: 3 (2 from this group)', quiz_num_attempt_summary($quiz, $cm, false, $groups['g1']->id)); + ] = $this->setup_users_course_groups([], $groupmode); + $cm->groupmode = $groupmode; // This is because quiz_num_attempt_summary expects the cm to have groupmode set. + foreach ($expected as $username => $data) { + $this->setUser($users[$username]); + foreach ($data as $result) { + $group = 0; + if (!is_null($result->group)) { + // If groups are set, we need to get the group id. + $group = $groups[$result->group]->id; + } + $attemptsummary = quiz_num_attempt_summary($quiz, $cm, false, $group); + $this->assertEquals( + $result->summary, + $attemptsummary, + "Failed for user $username with group {$result->group}" + ); + } + } + } - $this->setUser($users['teacher2']); - $this->assertEquals('Attempts: 3 (2 from your groups)', quiz_num_attempt_summary($quiz, $cm)); + /** + * Data provider for test_quiz_num_attempt_summary. + * + * @return array + */ + public static function num_attempts_data_provider(): array { + return [ + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + 't1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 't2' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5 (3 from your groups)'], + ], + 't3' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 's1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5 (3 from your groups)'], + ], + ], + ], + 'With no groups groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + 't1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 't2' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 't3' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 's1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + 't1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 't2' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5 (3 from your groups)'], + ], + 't3' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5'], + ], + 's1' => [ + (object) ['group' => 'g1', 'summary' => 'Attempts: 5 (3 from this group)'], + (object) ['group' => 'g2', 'summary' => 'Attempts: 5 (1 from this group)'], + (object) ['group' => null, 'summary' => 'Attempts: 5 (3 from your groups)'], + ], + ], + ], + ]; } /** * Test the quiz_num_attempts function. + * + * @param int $groupmode The group mode to use for the test. + * @param array $expected The expected results for each group setting. * @covers ::quiz_num_attempts + * @dataProvider quiz_num_attempts_data_provider */ - public function test_quiz_num_attempts(): void { + public function test_quiz_num_attempts(int $groupmode, array $expected): void { $this->resetAfterTest(); $this->setAdminUser(); [ @@ -1071,81 +1167,211 @@ final class lib_test extends \advanced_testcase { 'groups' => $groups, 'quiz' => $quiz, 'cm' => $cm, - ] = $this->setup_users_course_groups( - [ - 'user1' => ['student', 'g1', 2], - 'user2' => ['student', null, 1], - 'teacher1' => ['editingteacher', null, null], - 'teacher2' => ['teacher', 'g1', null], - ] - ); - // Check the summary. + ] = $this->setup_users_course_groups([], $groupmode); $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); - $this->setUser($users['teacher1']); - $this->assertEquals(['total' => 3], (array) quiz_num_attempts($cminfo)); - $this->assertEquals(['total' => 3, 'group' => 2], (array) quiz_num_attempts($cminfo, $groups['g1']->id)); + foreach ($expected as $result) { + $groupsstring = $result->groups ? implode(',', $result->groups) : '[]'; + $groupstocheck = array_map(fn($g) => $groups[$g]->id, $result->groups); + $numattempts = quiz_num_attempts($cminfo, $groupstocheck); - $this->setUser($users['teacher2']); - $this->assertEquals(['total' => 3, 'group' => 2], (array) quiz_num_attempts($cminfo)); + $this->assertEquals( + $result->numattempts, + $numattempts, + "Failed for group {$groupsstring}" + ); + } + } + + /** + * Data provider for test_quiz_num_attempt_summary. + * + * @return array + */ + public static function quiz_num_attempts_data_provider(): array { + return [ + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numattempts' => 3], // We count teachers. + (object) ['groups' => ['g1', 'g2'], 'numattempts' => 4], + (object) ['groups' => [], 'numattempts' => 5], + ], + ], + 'With no groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numattempts' => 3], + (object) ['groups' => ['g1', 'g2'], 'numattempts' => 4], + (object) ['groups' => [], 'numattempts' => 5], + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numattempts' => 3], + (object) ['groups' => ['g1', 'g2'], 'numattempts' => 4], + (object) ['groups' => [], 'numattempts' => 5], + ], + ], + ]; } /** * Test the quiz_num_users_who_attempted function. + * + * @param int $groupmode The group mode to use for the test. + * @param array $expected The expected results for each user. * @covers ::quiz_num_users_who_attempted + * @dataProvider quiz_num_users_who_attempted_data_provider */ - public function test_quiz_num_users_who_attempted(): void { + public function test_quiz_num_users_who_attempted(int $groupmode, array $expected): void { $this->resetAfterTest(); $this->setAdminUser(); [ 'cm' => $cm, - ] = $this->setup_users_course_groups( - [ - 'user1' => ['student', 'g1', 2], - 'user2' => ['student', null, 1], - 'teacher1' => ['editingteacher', null, null], - 'teacher2' => ['teacher', 'g1', 1], - ] - ); - // Check the summary. + 'groups' => $groups, + ] = $this->setup_users_course_groups([], $groupmode); $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); - $this->assertEquals(2, quiz_num_users_who_attempted($cminfo)); + foreach ($expected as $result) { + $groupsstring = $result->groups ? implode(',', $result->groups) : '[]'; + $groupstocheck = array_map(fn($g) => $groups[$g]->id, $result->groups); + $numuserattempted = quiz_num_users_who_attempted($cminfo, $groupstocheck); + $this->assertEquals( + $result->numusers, + $numuserattempted, + "Failed for group {$groupsstring}" + ); + } + } + /** + * Data provider for test_quiz_num_attempt_summary. + * + * @return array + */ + public static function quiz_num_users_who_attempted_data_provider(): array { + return [ + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], // Teacher not counted. + (object) ['groups' => ['g2'], 'numusers' => 1], // Student 3 only. + (object) ['groups' => ['g1', 'g2'], 'numusers' => 2], + (object) ['groups' => [], 'numusers' => 3], + ], + ], + 'With no groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], + (object) ['groups' => ['g2'], 'numusers' => 1], + (object) ['groups' => ['g1', 'g2'], 'numusers' => 2], + (object) ['groups' => [], 'numusers' => 3], + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], + (object) ['groups' => ['g2'], 'numusers' => 1], + (object) ['groups' => ['g1', 'g2'], 'numusers' => 2], + (object) ['groups' => [], 'numusers' => 3], + ], + ], + ]; } /** * Test the quiz_num_users_who_can_attempt function. + * + * We test here that the function returns the correct number of users who can attempt the quiz (i.e. users with the + * within the specified groups who have the capability to attempt the quiz). + * + * @param int $groupmode The group mode to use for the test. + * @param array $expected The expected results for each user. * @covers ::quiz_num_users_who_can_attempt + * @dataProvider quiz_num_users_who_can_attempt_data_provider */ - public function test_quiz_num_users_who_can_attempt(): void { + public function test_quiz_num_users_who_can_attempt(int $groupmode, array $expected): void { $this->resetAfterTest(); $this->setAdminUser(); [ 'cm' => $cm, - ] = $this->setup_users_course_groups( - [ - 'user1' => ['student', 'g1', 2], - 'user2' => ['student', null, 1], - 'user3' => ['student', 'g2', 1], - 'user4' => ['student', 'g2', 0], - 'teacher1' => ['editingteacher', null, null], - 'teacher2' => ['teacher', 'g1', null], - ] - ); + 'groups' => $groups, + ] = $this->setup_users_course_groups([], $groupmode); // Check the summary. $cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id); - $this->assertEquals(4, quiz_num_users_who_can_attempt($cminfo)); + foreach ($expected as $result) { + $groupsstring = $result->groups ? implode(',', $result->groups) : '[]'; + $groupstocheck = array_map(fn($g) => $groups[$g]->id, $result->groups); + $numusercanattempt = quiz_num_users_who_can_attempt($cminfo, $groupstocheck); + $this->assertEquals( + $result->numusers, + $numusercanattempt, + "Failed for group {$groupsstring}" + ); + } + } + + /** + * Data provider for test_quiz_num_attempt_summary. + * + * @return array + */ + public static function quiz_num_users_who_can_attempt_data_provider(): array { + return [ + 'With separate groups' => [ + 'groupmode' => SEPARATEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], // Teacher not counted. + (object) ['groups' => ['g2'], 'numusers' => 2], + (object) ['groups' => ['g1', 'g2'], 'numusers' => 3], + (object) ['groups' => [], 'numusers' => 4], // All students. + ], + ], + 'With no groups' => [ + 'groupmode' => NOGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], + (object) ['groups' => ['g2'], 'numusers' => 2], + (object) ['groups' => ['g1', 'g2'], 'numusers' => 3], + (object) ['groups' => [], 'numusers' => 4], + ], + ], + 'With visible groups' => [ + 'groupmode' => VISIBLEGROUPS, + 'expected' => [ + (object) ['groups' => ['g1'], 'numusers' => 1], + (object) ['groups' => ['g2'], 'numusers' => 2], + (object) ['groups' => ['g1', 'g2'], 'numusers' => 3], + (object) ['groups' => [], 'numusers' => 4], + ], + ], + ]; } /** * Set up users, course, groups and quiz for testing. * * @param array $data Array of user data with username as key and an array of role, group and attempts number as value. + * @param int $groupmode Group mode for the course, defaults to SEPARATEGROUPS. * @return array An array containing users, groups, quiz, course module and attempts. */ - private function setup_users_course_groups(array $data): array { + private function setup_users_course_groups(array $data, int $groupmode = SEPARATEGROUPS): array { $generator = $this->getDataGenerator(); + if (empty($data)) { + $data = [ + 's1' => ['student', 'g1', 2], + 's2' => ['student', null, 1], + 's3' => ['student', 'g2', 1], + 's4' => ['student', 'g2', 0], + 't1' => ['editingteacher', null, null], + 't2' => ['teacher', 'g1', 1], + 't3' => ['teacher', null, 0], + ]; + } // Create a course and a quiz. - $course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => SEPARATEGROUPS]); + $course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => $groupmode]); $quiz = $generator->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]); $cm = get_coursemodule_from_instance('quiz', $quiz->id);