MDL-86223 mod_quiz: Implement group based permissions
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user