MDL-83898 mod_quiz: Add overview page for quiz

This commit is contained in:
Laurent David
2025-08-27 11:50:13 +02:00
parent e3e51a0613
commit 907feb1498
8 changed files with 872 additions and 25 deletions
@@ -0,0 +1,9 @@
issueNumber: MDL-83898
notes:
mod_quiz:
- message: >-
Add helper methods in the mod/quiz/lib.php to count the number of
attempts (quiz_num_attempts), the number of users who attempted a quiz
(quiz_num_users_who_attempted) and users who can attempt
(quiz_num_users_who_can_attempt)
type: improved
@@ -0,0 +1,184 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\courseformat;
use core\output\renderer_helper;
use core\url;
use cm_info;
use core_calendar\output\humandate;
use core_courseformat\local\overview\overviewitem;
use core\output\action_link;
use core\output\local\properties\text_align;
use core\output\local\properties\button;
use core_courseformat\output\local\overview\overviewdialog;
use mod_quiz\dates;
use mod_quiz\quiz_settings;
/**
* Wiki overview integration.
*
* @package mod_quiz
* @copyright 2025 Laurent David <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overview extends \core_courseformat\activityoverviewbase {
/**
* @var quiz_settings the quiz settings object.
*/
private quiz_settings $quizsettings;
/**
* Constructor.
*
* @param cm_info $cm the course module instance.
* @param renderer_helper $rendererhelper the renderer helper.
*/
public function __construct(
cm_info $cm,
/** @var renderer_helper $rendererhelper the renderer helper */
protected readonly renderer_helper $rendererhelper,
) {
parent::__construct($cm);
$this->quizsettings = quiz_settings::create_for_cmid($cm->id);
}
#[\Override]
public function get_due_date_overview(): ?overviewitem {
global $USER;
$dates = new dates($this->cm, $USER->id);
$duedate = $dates->get_due_date();
$name = get_string('duedate', 'quiz');
if (empty($duedate)) {
return new overviewitem(
name: $name,
value: null,
content: '-',
);
}
$content = humandate::create_from_timestamp($duedate);
return new overviewitem(
name: $name,
value: $duedate,
content: $content,
);
}
#[\Override]
public function get_actions_overview(): ?overviewitem {
if (!has_capability('mod/quiz:viewreports', $this->cm->context)) {
return null;
}
$content = new action_link(
url: new url(
'/mod/quiz/report.php',
['id' => $this->cm->id, 'mode' => 'responses'],
),
text: get_string('view'),
attributes: ['class' => button::SECONDARY_OUTLINE->classes()],
);
return new overviewitem(
name: get_string('actions'),
value: '',
content: $content,
textalign: text_align::CENTER,
);
}
#[\Override]
public function get_extra_overview_items(): array {
global $CFG;
// Some extra items require global quiz functions.
require_once($CFG->dirroot . '/mod/quiz/lib.php');
return [
'studentswhoattempted' => $this->get_extra_students_who_attempted_overview(),
'totalattempts' => $this->get_extra_total_attempts_overview(),
];
}
/**
* Get the "Students who attempted" item.
*
* @return overviewitem|null The overview item.
*/
private function get_extra_students_who_attempted_overview(): ?overviewitem {
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);
$studentattemptedvalue = get_string(
'count_of_total',
'core',
['count' => $numstudentattempted, 'total' => $numstudentwhocanattempt]
);
return new overviewitem(
name: get_string('studentswhoattempted', 'mod_quiz'),
value: html_to_text($studentattemptedvalue),
content: $studentattemptedvalue,
textalign: text_align::END,
);
}
/**
* Get the "Total attempts" item.
*
* @return overviewitem|null The overview item.
*/
private function get_extra_total_attempts_overview(): ?overviewitem {
if (!has_capability('mod/quiz:viewreports', $this->cm->context)) {
return null;
}
$numattempts = quiz_num_attempts($this->cm);
$overviewdialog = new overviewdialog(
buttoncontent: $numattempts->total,
description: get_string('totalattempts', 'mod_quiz'),
definition: ['buttonclasses' => button::SECONDARY_OUTLINE->classes() . ' dropdown-toggle'],
);
$allowedattempts = $this->quizsettings->get_quiz()->attempts;
if ($allowedattempts == 0) {
$allowedattempts = get_string('attemptsunlimited', 'mod_quiz');
}
$overviewdialog->add_item(
get_string('allowedattemptsperstudent', 'mod_quiz'),
$allowedattempts,
);
$numstudentattempted = quiz_num_users_who_attempted($this->cm);
$numstudentwhocanattempt = quiz_num_users_who_can_attempt($this->cm);
if ($numstudentwhocanattempt > 0 && $numstudentattempted > 0) {
$overviewdialog->add_item(
get_string('averageattemptsperstudent', 'mod_quiz'),
round($numstudentattempted / $numstudentwhocanattempt)
);
}
return new overviewitem(
name: get_string('totalattempts', 'mod_quiz'),
value: $numattempts->total,
content: $overviewdialog,
textalign: text_align::START,
);
}
}
+16
View File
@@ -36,6 +36,9 @@ use core\activity_dates;
*/
class dates extends activity_dates {
/** @var int|null timeclose the activity closing date */
private ?int $timeclose;
/**
* Returns a list of important dates in mod_quiz
*
@@ -44,6 +47,8 @@ class dates extends activity_dates {
protected function get_dates(): array {
$timeopen = $this->cm->customdata['timeopen'] ?? null;
$timeclose = $this->cm->customdata['timeclose'] ?? null;
$this->timeclose = $timeclose ? (int) $timeclose : null;
$now = time();
$dates = [];
@@ -67,4 +72,15 @@ class dates extends activity_dates {
return $dates;
}
/**
* Returns the dues date data, if any.
* @return int|null the close timestamp or null if not set.
*/
public function get_due_date(): ?int {
if (!isset($this->timeclose)) {
$this->get_dates();
}
return $this->timeclose;
}
}
+5
View File
@@ -67,6 +67,7 @@ $string['afternquestions'] = 'After adding {$a} questions';
$string['age'] = 'age';
$string['allattempts'] = 'All attempts';
$string['allinone'] = 'Unlimited';
$string['allowedattemptsperstudent'] = 'Allowed attempts per student';
$string['allowreview'] = 'Allow review';
$string['alreadysubmitted'] = 'It is likely that you have already submitted this attempt';
$string['alternativeunits'] = 'Alternative units';
@@ -120,6 +121,7 @@ $string['attempttitle'] = '{$a}';
$string['attempttitlepaged'] = '{$a->name} (page {$a->currentpage} of {$a->totalpages})';
$string['autosaveperiod'] = 'Auto-save delay';
$string['autosaveperiod_desc'] = 'Responses can be saved automatically during quiz attempts. The responses are saved whenever one is changed, and then after this delay. There is a trade-off: a shorter delay increases the server load, but reduces the chance that students lose their work. If you are going to make this delay much shorter, you should change the value gradually and monitor the server load. If the load gets too high, make the delay longer again. Setting the delay to 0 turns off auto-saving.';
$string['averageattemptsperstudent'] = 'Average attempts per student';
$string['back'] = 'Back to preview question';
$string['backtocourse'] = 'Back to the course';
$string['backtoquestionlist'] = 'Back to question list';
@@ -283,6 +285,7 @@ $string['download'] = 'Click to download the exported category file';
$string['downloadextra'] = '(file is also stored in the course files in the /backupdata/quiz folder)';
$string['dragtoafter'] = 'After {$a}';
$string['dragtostart'] = 'To the start';
$string['duedate'] = 'Due date';
$string['duplicateresponse'] = 'This submission has been ignored because you gave an equivalent answer earlier.';
$string['eachattemptbuildsonthelast'] = 'Each attempt builds on the last';
$string['eachattemptbuildsonthelast_help'] = 'If multiple attempts are allowed and this setting is enabled, each new quiz attempt will contain the results of the previous attempt. This allows a quiz to be completed over several attempts.';
@@ -1075,6 +1078,7 @@ $string['stateoverdue'] = 'Overdue';
$string['stateoverduedetails'] = 'Must be submitted by {$a}';
$string['statesubmitted'] = 'Submitted';
$string['status'] = 'Status';
$string['studentswhoattempted'] = 'Students who attempted';
$string['stoponerror'] = 'Stop on error';
$string['submission_confirmation'] = 'Submit all your answers and finish?';
$string['submission_confirmation_unanswered'] = 'Questions without a response: {$a}';
@@ -1107,6 +1111,7 @@ $string['tofile'] = 'to file';
$string['tolerance'] = 'Tolerance';
$string['toomanyrandom'] = 'The number of random questions required is more than are still available in the category!';
$string['top'] = 'Top';
$string['totalattempts'] = 'Total attempts';
$string['totalmarks'] = 'Total of marks';
$string['totalmarksx'] = 'Total of marks: {$a}';
$string['totalquestionsinrandomqcategory'] = 'Total of {$a} questions in category.';
+90 -25
View File
@@ -1568,32 +1568,97 @@ function quiz_reset_userdata($data) {
* "Attemtps 123 (45 from this group)".
*/
function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
global $DB, $USER;
$numattempts = $DB->count_records('quiz_attempts', ['quiz' => $quiz->id, 'preview' => 0]);
if ($numattempts || $returnzero) {
if (groups_get_activity_groupmode($cm)) {
$a = new stdClass();
$a->total = $numattempts;
if ($currentgroup) {
$a->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 = ?',
[$quiz->id, $currentgroup]);
return get_string('attemptsnumthisgroup', 'quiz', $a);
} else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
[$usql, $params] = $DB->get_in_or_equal(array_keys($groups));
$a->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([$quiz->id], $params));
return get_string('attemptsnumyourgroups', 'quiz', $a);
}
}
return get_string('attemptsnum', 'quiz', $numattempts);
global $USER;
[$course, $fullcminfo] = get_course_and_cm_from_instance($quiz, 'quiz');
$numattempts = quiz_num_attempts($fullcminfo, $currentgroup);
if (!$numattempts->total && !$returnzero) {
return '';
}
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);
}
}
return get_string('attemptsnum', 'quiz', $numattempts->total);
}
/**
* 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.
*/
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;
}
/**
* Return a number of users who have attempted a particular quiz,
* Returns 0 if no attempts have been made yet.
*
* @param cm_info $cm
* @return int
*/
function quiz_num_users_who_attempted(cm_info $cm): 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);
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,
);
}
/**
* Return a number of users who can attempt a particular quiz,
*
* @param cm_info $cm
* @return int
*/
function quiz_num_users_who_can_attempt(cm_info $cm): 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']);
return $DB->count_records_sql(
"SELECT COUNT(DISTINCT u.id)
FROM {user} u
$studentsjoins->joins
WHERE $studentsjoins->wheres",
$studentsjoins->params,
);
}
/**
@@ -0,0 +1,78 @@
@mod @mod_quiz
Feature: Testing overview_report in mod_quiz
In order to list all quiz in a course
As a user
I need to be able to see the quiz overview
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Username | 1 |
| student2 | Username | 2 |
| student3 | Username | 3 |
| teacher1 | Teacher | T |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | idnumber | timeclose |
| quiz | Quiz 1 | C1 | quiz1 | ##tomorrow## |
| quiz | Quiz 2 | C1 | quiz2 | 0 |
| qbank | Qbank 1 | C1 | qbank1 | |
And the following "question categories" exist:
| contextlevel | reference | name |
| Activity module | qbank1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | TF1 | First question |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2.00 |
And quiz "Quiz 2" contains the following questions:
| question | page | maxmark |
| TF1 | 1 | 2.00 |
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
And user "student2" has attempted "Quiz 2" with responses:
| slot | response |
| 1 | True |
@javascript
Scenario: Teacher can see the quiz relevant information in the quiz overview
When I am on the "Course 1" "course > activities > quiz" page logged in as "teacher1"
Then the following should exist in the "Table listing all Quiz activities" table:
| Name | Students who attempted | Total attempts | Due date |
| Quiz 1 | 2 of 3 | 2 | Tomorrow |
| Quiz 2 | 1 of 3 | 1 | - |
And I click on "2" "button" in the "Quiz 1" "table_row"
And I should see "Allowed attempts per student: Unlimited attempts"
And I should see "Average attempts per student: 1"
And I press the escape key
And "0" "button" should not exist in the "Quiz 2" "table_row"
And I click on "1" "button" in the "Quiz 2" "table_row"
And I should see "Allowed attempts per student: Unlimited attempts"
And I should see "Average attempts per student: 0"
And I press the escape key
And I click on "View" "link" in the "Quiz 1" "table_row"
And I should see "Results" in the "page-header" "region"
Scenario: Students can see the quiz relevant information in the quiz overview
When I am on the "Course 1" "course > activities > quiz" page logged in as "student1"
Then I should not see "Actions" in the "quiz_overview_collapsible" "region"
And I should not see "Students who attempted" in the "quiz_overview_collapsible" "region"
And I should not see "Total attempts" in the "quiz_overview_collapsible" "region"
And the following should exist in the "Table listing all Quiz activities" table:
| Name | Due date | Grade |
| Quiz 1 | Tomorrow | 100 |
| Quiz 2 | - | - |
@@ -0,0 +1,324 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\courseformat;
use core_courseformat\local\overview\overviewfactory;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
/**
* Tests for Lesson overview.
*
* @package mod_quiz
* @category test
* @copyright 2025 Laurent David <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\courseformat\overview
*/
final class overview_test extends \advanced_testcase {
/**
* Test get_due_date_overview.
*
* @param int|null $timeincrement the time increment in seconds to add to the current time for the deadline.
* @return void
* @dataProvider provider_test_get_due_date_overview
**/
public function test_get_due_date_overview(?int $timeincrement): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$timeclose = $timeincrement ? $this->mock_clock_with_frozen()->time() + $timeincrement : 0;
$quiz = $this->getDataGenerator()->create_module(
'quiz',
[
'course' => $course->id,
'timeclose' => $timeclose,
],
);
$this->setUser($student);
$cm = get_fast_modinfo($course)->get_cm($quiz->cmid);
$item = overviewfactory::create($cm)->get_due_date_overview();
$this->assertEquals(get_string('duedate', 'quiz'), $item->get_name());
$this->assertEquals($timeclose, $item->get_value());
}
/**
* Provider for test_get_due_date_overview.
*
* @return array
*/
public static function provider_test_get_due_date_overview(): array {
return [
'no_due' => [
'timeincrement' => null,
],
'past_due' => [
'timeincrement' => -1 * (4 * DAYSECS),
],
'future_due' => [
'timeincrement' => (4 * DAYSECS),
],
];
}
/**
* Test get_actions_overview.
*
* @param string $currentuser
* @param array|null $expected
* @return void
* @dataProvider provider_test_get_actions_overview
*/
public function test_get_actions_overview(
string $currentuser,
?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);
$item = overviewfactory::create($cminfo)->get_actions_overview();
if ($expected === null) {
$this->assertNull($item);
return;
}
$this->assertEquals(
$expected,
['name' => $item->get_name(), 'value' => $item->get_value()]
);
}
/**
* Data provider for test_get_actions_overview.
*
* @return array
*/
public static function provider_test_get_actions_overview(): array {
return [
'Student' => [
'currentuser' => 's1',
'expected' => null,
],
'Teacher' => [
'currentuser' => 't1',
'expected' => [
'name' => get_string('actions'),
'value' => '',
],
],
];
}
/**
* Test get_total_attempts_overview.
*
* @param string $currentuser
* @param string|null $expected
* @return void
* @dataProvider provider_test_get_total_attempts_overview
*/
public function test_get_extra_totalattempts_overview(
string $currentuser,
?string $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);
$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;
}
$this->assertEquals(
$expected,
$item->get_value()
);
}
/**
* Data provider for provider_test_get_total_attempts_overview.
*
* @return array
*/
public static function provider_test_get_total_attempts_overview(): array {
return [
'Teacher t1' => [
'currentuser' => 't1',
'expected' => "3",
],
'Student' => [
'currentuser' => 's1',
'expected' => null,
],
];
}
/**
* Test get_students_who_attempted_overview.
*
* @param string $currentuser
* @param ?string $expected
* @return void
* @dataProvider provider_test_get_students_who_attempted_overview
**/
public function test_get_extra_attemptedstudents_overview(
string $currentuser,
?string $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);
$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;
}
$this->assertEquals(
$expected,
$item->get_value()
);
}
/**
* Data provider for test_get_students_who_attempted_overview.
*
* @return array
*/
public static function provider_test_get_students_who_attempted_overview(): array {
return [
'Teacher' => [
'currentuser' => 't1',
'expected' => "2 of 2",
],
'Student' => [
'currentuser' => 's1',
'expected' => null,
],
];
}
/**
* 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.
* @return array An array containing users, groups, quiz, course module and attempts.
*/
private function setup_users_course_groups(array $data): array {
$generator = $this->getDataGenerator();
// Create a course and a quiz.
$course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => SEPARATEGROUPS]);
$quiz = $generator->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Add a question to the quiz.
$questiongenerator = $generator->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Create users and groups.
$groups = [
'g1' => $generator->create_group(['courseid' => $course->id, 'name' => 'g1']),
'g2' => $generator->create_group(['courseid' => $course->id, 'name' => 'g2']),
];
$users = [];
$attempts = [];
foreach ($data as $username => [$role, $group, $attemptsnum]) {
$users[$username] = $generator->create_and_enrol($course, $role, ['username' => $username]);
if ($group) {
$generator->create_group_member(['userid' => $users[$username]->id, 'groupid' => $groups[$group]->id]);
}
if ($attemptsnum) {
// Create attempts for the user.
for ($acount = 1; $acount <= $attemptsnum; $acount++) {
$quizobj = quiz_settings::create($quiz->id, $users[$username]->id);
// Create an attempt for the student in the quiz.
$timenow = time();
$attempt = quiz_create_attempt($quizobj, $acount, false, $timenow, false, $users[$username]->id);
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submit($timenow, false);
$attemptobj->process_grade_submission($timenow);
$attempts[] = $attempt;
}
}
}
return [
'users' => $users,
'groups' => $groups,
'quiz' => $quiz,
'cm' => $cm,
'course' => $course,
'attempts' => $attempts,
];
}
}
+166
View File
@@ -1029,4 +1029,170 @@ final class lib_test extends \advanced_testcase {
$this->assertEquals($newvalue, $result['value']);
}
}
/**
* Test the quiz_num_attempt_summary function.
* @covers ::quiz_num_attempt_summary
*/
public function test_quiz_num_attempt_summary(): void {
$this->resetAfterTest();
$this->setAdminUser();
[
'users' => $users,
'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->setUser($users['teacher2']);
$this->assertEquals('Attempts: 3 (2 from your groups)', quiz_num_attempt_summary($quiz, $cm));
}
/**
* Test the quiz_num_attempts function.
* @covers ::quiz_num_attempts
*/
public function test_quiz_num_attempts(): void {
$this->resetAfterTest();
$this->setAdminUser();
[
'users' => $users,
'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.
$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));
$this->setUser($users['teacher2']);
$this->assertEquals(['total' => 3, 'group' => 2], (array) quiz_num_attempts($cminfo));
}
/**
* Test the quiz_num_users_who_attempted function.
* @covers ::quiz_num_users_who_attempted
*/
public function test_quiz_num_users_who_attempted(): 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.
$cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id);
$this->assertEquals(2, quiz_num_users_who_attempted($cminfo));
}
/**
* Test the quiz_num_users_who_can_attempt function.
* @covers ::quiz_num_users_who_can_attempt
*/
public function test_quiz_num_users_who_can_attempt(): 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],
]
);
// Check the summary.
$cminfo = get_fast_modinfo($cm->course)->get_cm($cm->id);
$this->assertEquals(4, quiz_num_users_who_can_attempt($cminfo));
}
/**
* 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.
* @return array An array containing users, groups, quiz, course module and attempts.
*/
private function setup_users_course_groups(array $data): array {
$generator = $this->getDataGenerator();
// Create a course and a quiz.
$course = $generator->create_course(['groupmodeforce' => 1, 'groupmode' => SEPARATEGROUPS]);
$quiz = $generator->create_module('quiz', ['course' => $course->id, 'sumgrades' => 1]);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
// Add a question to the quiz.
$questiongenerator = $generator->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
quiz_add_quiz_question($question->id, $quiz);
// Create users and groups.
$groups = [
'g1' => $generator->create_group(['courseid' => $course->id, 'name' => 'g1']),
'g2' => $generator->create_group(['courseid' => $course->id, 'name' => 'g2']),
];
$users = [];
$attempts = [];
foreach ($data as $username => [$role, $group, $attemptsnum]) {
$users[$username] = $generator->create_and_enrol($course, $role, ['username' => $username]);
if ($group) {
$generator->create_group_member(['userid' => $users[$username]->id, 'groupid' => $groups[$group]->id]);
}
if ($attemptsnum) {
for ($acount = 1; $acount <= $attemptsnum; $acount++) {
$quizobj = quiz_settings::create($quiz->id, $users[$username]->id);
// Create an attempt for the student in the quiz.
$timenow = time();
$attempt = quiz_create_attempt($quizobj, $acount, false, $timenow, false, $users[$username]->id);
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
// Finish the attempt.
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submit($timenow, false);
$attemptobj->process_grade_submission($timenow);
$attempts[] = $attempt;
}
}
}
return [
'users' => $users,
'groups' => $groups,
'quiz' => $quiz,
'cm' => $cm,
'attempts' => $attempts,
];
}
}