MDL-86223 mod_quiz: Implement group based permissions

This commit is contained in:
Laurent David
2025-09-01 11:40:26 +02:00
parent a1a8af4ca0
commit d976368a08
5 changed files with 492 additions and 191 deletions
@@ -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
@@ -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,
);
+79 -47
View File
@@ -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.
@@ -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);
+284 -58
View File
@@ -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);