diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index b59e4048403..714926d2e71 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -616,6 +616,11 @@ $string['overrides'] = 'Overrides'; $string['overridesforquiz'] = 'Settings overrides: {$a}'; $string['overridesnoneforgroups'] = 'No group settings overrides have been created for this quiz.'; $string['overridesnoneforusers'] = 'No user settings overrides have been created for this quiz.'; +$string['overridessummary'] = 'Settings overrides exist ({$a})'; +$string['overridessummarythisgroup'] = 'Settings overrides exist ({$a}) for this group'; +$string['overridessummaryyourgroups'] = 'Settings overrides exist ({$a}) for your groups'; +$string['overridessummarygroup'] = 'Groups: {$a}'; +$string['overridessummaryuser'] = 'Users: {$a}'; $string['overrideuser'] = 'Override user'; $string['overrideusereventname'] = '{$a->quiz} - Override'; $string['pageshort'] = 'P'; diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 8f377d16479..37873c60a4b 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -1645,16 +1645,10 @@ function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup */ function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) { - global $CFG; - $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); - if (!$summary) { - return ''; - } + global $PAGE; - require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); - $url = new moodle_url('/mod/quiz/report.php', array( - 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); - return html_writer::link($url, $summary); + return $PAGE->get_renderer('mod_quiz')->quiz_attempt_summary_link_to_reports( + $quiz, $cm, $context, $returnzero, $currentgroup); } /** diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 7768db86c29..ac304014898 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -968,6 +968,72 @@ function quiz_update_all_final_grades($quiz) { } } +/** + * Return summary of the number of settings override that exist. + * + * To get a nice display of this, see the quiz_override_summary_links() + * quiz renderer method. + * + * @param stdClass $quiz the quiz settings. Only $quiz->id is used at the moment. + * @param stdClass|cm_info $cm the cm object. Only $cm->course, $cm->groupmode and + * $cm->groupingid fields are used at the moment. + * @param int $currentgroup if there is a concept of current group where this method is being called + * (e.g. a report) pass it in here. Default 0 which means no current group. + * @return array like 'group' => 3, 'user' => 12] where 3 is the number of group overrides, + * and 12 is the number of user ones. + */ +function quiz_override_summary(stdClass $quiz, stdClass $cm, int $currentgroup = 0): array { + global $DB; + + if ($currentgroup) { + // Currently only interested in one group. + $groupcount = $DB->count_records('quiz_overrides', ['quiz' => $quiz->id, 'groupid' => $currentgroup]); + $usercount = $DB->count_records_sql(" + SELECT COUNT(1) + FROM {quiz_overrides} o + JOIN {groups_members} gm ON o.userid = gm.userid + WHERE o.quiz = ? + AND gm.groupid = ? + ", [$quiz->id, $currentgroup]); + return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'onegroup']; + } + + $quizgroupmode = groups_get_activity_groupmode($cm); + $accessallgroups = ($quizgroupmode == NOGROUPS) || + has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)); + + if ($accessallgroups) { + // User can see all groups. + $groupcount = $DB->count_records_select('quiz_overrides', + 'quiz = ? AND groupid IS NOT NULL', [$quiz->id]); + $usercount = $DB->count_records_select('quiz_overrides', + 'quiz = ? AND userid IS NOT NULL', [$quiz->id]); + return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'allgroups']; + + } else { + // User can only see groups they are in. + $groups = groups_get_activity_allowed_groups($cm); + if (!$groups) { + return ['group' => 0, 'user' => 0, 'mode' => 'somegroups']; + } + + list($groupidtest, $params) = $DB->get_in_or_equal(array_keys($groups)); + $params[] = $quiz->id; + + $groupcount = $DB->count_records_select('quiz_overrides', + "groupid $groupidtest AND quiz = ?", $params); + $usercount = $DB->count_records_sql(" + SELECT COUNT(1) + FROM {quiz_overrides} o + JOIN {groups_members} gm ON o.userid = gm.userid + WHERE gm.groupid $groupidtest + AND o.quiz = ? + ", $params); + + return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'somegroups']; + } +} + /** * Efficiently update check state time on all open attempts * diff --git a/mod/quiz/overrides.php b/mod/quiz/overrides.php index 6e0e0dbb579..30b5fee311c 100644 --- a/mod/quiz/overrides.php +++ b/mod/quiz/overrides.php @@ -102,6 +102,7 @@ if ($groupmode) { $overrides = $DB->get_records_sql($sql, $params); } } else { + // User overrides. $colname = get_string('user'); list($sort, $params) = users_order_by_sql('u'); $params['quizid'] = $quiz->id; diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 363159661f1..33a316476ff 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -927,7 +927,7 @@ class mod_quiz_renderer extends plugin_renderer_base { * * @param object $quiz the quiz settings. * @param object $cm the course_module object. - * @param object $context the quiz context. + * @param context $context the quiz context. * @param array $messages any access messages that should be described. * @return string HTML to output. */ @@ -953,6 +953,13 @@ class mod_quiz_renderer extends plugin_renderer_base { array('class' => 'quizattemptcounts')); } } + + if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) { + if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) { + $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']); + } + } + return $output; } @@ -1216,15 +1223,14 @@ class mod_quiz_renderer extends plugin_renderer_base { * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link * to the quiz reports. * - * @param object $quiz the quiz object. Only $quiz->id is used at the moment. - * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid + * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. + * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid * fields are used at the moment. - * @param object $context the quiz context. + * @param context $context the quiz context. * @param bool $returnzero if false (default), when no attempts have been made '' is returned - * instead of 'Attempts: 0'. + * instead of 'Attempts: 0'. * @param int $currentgroup if there is a concept of current group where this method is being - * called - * (e.g. a report) pass it in here. Default 0 which means no current group. + * called (e.g. a report) pass it in here. Default 0 which means no current group. * @return string HTML fragment for the link. */ public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, @@ -1241,6 +1247,49 @@ class mod_quiz_renderer extends plugin_renderer_base { return html_writer::link($url, $summary); } + /** + * Render a summary of the number of group and user overrides, with corresponding links. + * + * @param stdClass $quiz the quiz settings. + * @param stdClass|cm_info $cm the cm object. + * @param int $currentgroup currently selected group, if there is one. + * @return string HTML fragment for the link. + */ + public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string { + + $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); + $counts = quiz_override_summary($quiz, $cm, $currentgroup); + + $links = []; + if ($counts['group']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']), + get_string('overridessummarygroup', 'quiz', $counts['group'])); + } + if ($counts['user']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']), + get_string('overridessummaryuser', 'quiz', $counts['user'])); + } + + if (!$links) { + return ''; + } + + $links = implode(', ', $links); + switch ($counts['mode']) { + case 'onegroup': + return get_string('overridessummarythisgroup', 'quiz', $links); + + case 'somegroups': + return get_string('overridessummaryyourgroups', 'quiz', $links); + + case 'allgroups': + return get_string('overridessummary', 'quiz', $links); + + default: + throw new coding_exception('Unexpected mode ' . $counts['mode']); + } + } + /** * Outputs a chart. * diff --git a/mod/quiz/tests/behat/quiz_group_override.feature b/mod/quiz/tests/behat/quiz_group_override.feature index 04edfc4e83a..f9586310db0 100644 --- a/mod/quiz/tests/behat/quiz_group_override.feature +++ b/mod/quiz/tests/behat/quiz_group_override.feature @@ -82,8 +82,10 @@ Feature: Quiz group override | quiz | group | attempts | | Test quiz | G1 | 2 | | Test quiz | G2 | 2 | - When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "teacher1" - Then "Group 1" "table_row" should exist + When I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1" + Then I should see "Settings overrides exist (Groups: 2)" + And I follow "Groups: 2" + And "Group 1" "table_row" should exist And "Group 2" "table_row" should exist Scenario: A teacher without accessallgroups permission should only see the group overrides within his/her groups, when the activity's group mode is "separate groups" @@ -94,7 +96,9 @@ Feature: Quiz group override | quiz | group | attempts | | Test quiz | G1 | 2 | | Test quiz | G2 | 2 | - When I am on the "Test quiz" "mod_quiz > Group overrides" page logged in as "teacher1" + When I am on the "Test quiz" "mod_quiz > View" page logged in as "teacher1" + Then I should see "Settings overrides exist (Groups: 1) for your groups" + And I follow "Groups: 1" Then "Group 1" "table_row" should exist And "Group 2" "table_row" should not exist diff --git a/mod/quiz/tests/behat/quiz_user_override.feature b/mod/quiz/tests/behat/quiz_user_override.feature index c9e7d905834..5e6ecda87f8 100644 --- a/mod/quiz/tests/behat/quiz_user_override.feature +++ b/mod/quiz/tests/behat/quiz_user_override.feature @@ -121,3 +121,5 @@ Feature: Quiz user override And "Edit" "link" should not exist in the "Student One" "table_row" And "Copy" "link" should not exist in the "Student One" "table_row" And "Delete" "link" should not exist in the "Student One" "table_row" + And I am on the "Test quiz" "mod_quiz > View" page + And I should see "Settings overrides exist (Users: 2)" diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php index 7b9ee84217d..c5f5b675338 100644 --- a/mod/quiz/tests/locallib_test.php +++ b/mod/quiz/tests/locallib_test.php @@ -839,4 +839,106 @@ class mod_quiz_locallib_testcase extends advanced_testcase { $this->assertEquals($formattedexptected, $actual); } + + public function test_quiz_override_summary() { + global $DB, $PAGE; + $this->resetAfterTest(); + $generator = $this->getDataGenerator(); + /** @var mod_quiz_generator $quizgenerator */ + $quizgenerator = $generator->get_plugin_generator('mod_quiz'); + /** @var mod_quiz_renderer $renderer */ + $renderer = $PAGE->get_renderer('mod_quiz'); + + // Course with quiz and a group - plus some others, to verify they don't get counted. + $course = $generator->create_course(); + $quiz = $quizgenerator->create_instance(['course' => $course->id, 'groupmode' => SEPARATEGROUPS]); + $cm = get_coursemodule_from_id('quiz', $quiz->cmid, $course->id); + $group = $generator->create_group(['courseid' => $course->id]); + $othergroup = $generator->create_group(['courseid' => $course->id]); + $otherquiz = $quizgenerator->create_instance(['course' => $course->id]); + + // Initial test (as admin) with no data. + $this->setAdminUser(); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'allgroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'], + quiz_override_summary($quiz, $cm, $group->id)); + + // Editing teacher. + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); + + // Non-editing teacher. + $tutor = $generator->create_user(); + $generator->enrol_user($tutor->id, $course->id, 'teacher'); + $generator->create_group_member(['userid' => $tutor->id, 'groupid' => $group->id]); + + // Three students. + $student1 = $generator->create_user(); + $generator->enrol_user($student1->id, $course->id, 'student'); + $generator->create_group_member(['userid' => $student1->id, 'groupid' => $group->id]); + + $student2 = $generator->create_user(); + $generator->enrol_user($student2->id, $course->id, 'student'); + $generator->create_group_member(['userid' => $student2->id, 'groupid' => $othergroup->id]); + + $student3 = $generator->create_user(); + $generator->enrol_user($student3->id, $course->id, 'student'); + + // Initial test now users exist, but before overrides. + // Test as teacher. + $this->setUser($teacher); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'allgroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'], + quiz_override_summary($quiz, $cm, $group->id)); + + // Test as tutor. + $this->setUser($tutor); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'somegroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals(['group' => 0, 'user' => 0, 'mode' => 'onegroup'], + quiz_override_summary($quiz, $cm, $group->id)); + $this->assertEquals('', $renderer->quiz_override_summary_links($quiz, $cm)); + + // Quiz setting overrides for students 1 and 3. + $quizgenerator->create_override(['quiz' => $quiz->id, 'userid' => $student1->id, 'attempts' => 2]); + $quizgenerator->create_override(['quiz' => $quiz->id, 'userid' => $student3->id, 'attempts' => 2]); + $quizgenerator->create_override(['quiz' => $quiz->id, 'groupid' => $group->id, 'attempts' => 3]); + $quizgenerator->create_override(['quiz' => $quiz->id, 'groupid' => $othergroup->id, 'attempts' => 3]); + $quizgenerator->create_override(['quiz' => $otherquiz->id, 'userid' => $student2->id, 'attempts' => 2]); + + // Test as teacher. + $this->setUser($teacher); + $this->assertEquals(['group' => 2, 'user' => 2, 'mode' => 'allgroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals('Settings overrides exist (Groups: 2, Users: 2)', + // Links checked by Behat, so strip them for these tests. + html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false)); + $this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'onegroup'], + quiz_override_summary($quiz, $cm, $group->id)); + $this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for this group', + html_to_text($renderer->quiz_override_summary_links($quiz, $cm, $group->id), 0, false)); + + // Test as tutor. + $this->setUser($tutor); + $this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'somegroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for your groups', + html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false)); + $this->assertEquals(['group' => 1, 'user' => 1, 'mode' => 'onegroup'], + quiz_override_summary($quiz, $cm, $group->id)); + $this->assertEquals('Settings overrides exist (Groups: 1, Users: 1) for this group', + html_to_text($renderer->quiz_override_summary_links($quiz, $cm, $group->id), 0, false)); + + // Now set the quiz to be group mode: no groups, and re-test as tutor. + // In this case, the tutor should see all groups. + $DB->set_field('course_modules', 'groupmode', NOGROUPS, ['id' => $cm->id]); + $cm = get_coursemodule_from_id('quiz', $quiz->cmid, $course->id); + + $this->assertEquals(['group' => 2, 'user' => 2, 'mode' => 'allgroups'], + quiz_override_summary($quiz, $cm)); + $this->assertEquals('Settings overrides exist (Groups: 2, Users: 2)', + html_to_text($renderer->quiz_override_summary_links($quiz, $cm), 0, false)); + } }