MDL-68652 core_grades: Refactor grades functions.
[moodle.git] / grade / tests / lib_test.php
blob19417763c5bbc8e84854cff2b57683a66ec4db27
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Unit tests for grade/lib.php.
20 * @package core_grades
21 * @category test
22 * @copyright 2016 Jun Pataleta
23 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
25 namespace core_grades;
27 use assign;
28 use cm_info;
29 use grade_item;
30 use grade_plugin_return;
31 use grade_report_grader;
33 defined('MOODLE_INTERNAL') || die();
35 global $CFG;
36 require_once($CFG->dirroot . '/grade/lib.php');
38 /**
39 * Unit tests for grade/lib.php.
41 * @package core_grades
42 * @category test
43 * @copyright 2016 Jun Pataleta <jun@moodle.com>
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 class lib_test extends \advanced_testcase {
48 /**
49 * Test can_output_item.
51 public function test_can_output_item() {
52 $this->resetAfterTest();
54 $generator = $this->getDataGenerator();
56 // Course level grade category.
57 $course = $generator->create_course();
58 // Grade tree looks something like:
59 // - Test course (Rendered).
60 $gradetree = \grade_category::fetch_course_tree($course->id);
61 $this->assertTrue(\grade_tree::can_output_item($gradetree));
63 // Add a grade category with default settings.
64 $generator->create_grade_category(array('courseid' => $course->id));
65 // Grade tree now looks something like:
66 // - Test course n (Rendered).
67 // -- Grade category n (Rendered).
68 $gradetree = \grade_category::fetch_course_tree($course->id);
69 $this->assertNotEmpty($gradetree['children']);
70 foreach ($gradetree['children'] as $child) {
71 $this->assertTrue(\grade_tree::can_output_item($child));
74 // Add a grade category with grade type = None.
75 $nototalcategory = 'No total category';
76 $nototalparams = [
77 'courseid' => $course->id,
78 'fullname' => $nototalcategory,
79 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN
81 $nototal = $generator->create_grade_category($nototalparams);
82 $catnototal = \grade_category::fetch(array('id' => $nototal->id));
83 // Set the grade type of the grade item associated to the grade category.
84 $catitemnototal = $catnototal->load_grade_item();
85 $catitemnototal->gradetype = GRADE_TYPE_NONE;
86 $catitemnototal->update();
88 // Grade tree looks something like:
89 // - Test course n (Rendered).
90 // -- Grade category n (Rendered).
91 // -- No total category (Not rendered).
92 $gradetree = \grade_category::fetch_course_tree($course->id);
93 foreach ($gradetree['children'] as $child) {
94 if ($child['object']->fullname == $nototalcategory) {
95 $this->assertFalse(\grade_tree::can_output_item($child));
96 } else {
97 $this->assertTrue(\grade_tree::can_output_item($child));
101 // Add another grade category with default settings under 'No total category'.
102 $normalinnototalparams = [
103 'courseid' => $course->id,
104 'fullname' => 'Normal category in no total category',
105 'parent' => $nototal->id
107 $generator->create_grade_category($normalinnototalparams);
109 // Grade tree looks something like:
110 // - Test course n (Rendered).
111 // -- Grade category n (Rendered).
112 // -- No total category (Rendered).
113 // --- Normal category in no total category (Rendered).
114 $gradetree = \grade_category::fetch_course_tree($course->id);
115 foreach ($gradetree['children'] as $child) {
116 // All children are now visible.
117 $this->assertTrue(\grade_tree::can_output_item($child));
118 if (!empty($child['children'])) {
119 foreach ($child['children'] as $grandchild) {
120 $this->assertTrue(\grade_tree::can_output_item($grandchild));
125 // Add a grade category with grade type = None.
126 $nototalcategory2 = 'No total category 2';
127 $nototal2params = [
128 'courseid' => $course->id,
129 'fullname' => $nototalcategory2,
130 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN
132 $nototal2 = $generator->create_grade_category($nototal2params);
133 $catnototal2 = \grade_category::fetch(array('id' => $nototal2->id));
134 // Set the grade type of the grade item associated to the grade category.
135 $catitemnototal2 = $catnototal2->load_grade_item();
136 $catitemnototal2->gradetype = GRADE_TYPE_NONE;
137 $catitemnototal2->update();
139 // Add a category with no total under 'No total category'.
140 $nototalinnototalcategory = 'Category with no total in no total category';
141 $nototalinnototalparams = [
142 'courseid' => $course->id,
143 'fullname' => $nototalinnototalcategory,
144 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN,
145 'parent' => $nototal2->id
147 $nototalinnototal = $generator->create_grade_category($nototalinnototalparams);
148 $catnototalinnototal = \grade_category::fetch(array('id' => $nototalinnototal->id));
149 // Set the grade type of the grade item associated to the grade category.
150 $catitemnototalinnototal = $catnototalinnototal->load_grade_item();
151 $catitemnototalinnototal->gradetype = GRADE_TYPE_NONE;
152 $catitemnototalinnototal->update();
154 // Grade tree looks something like:
155 // - Test course n (Rendered).
156 // -- Grade category n (Rendered).
157 // -- No total category (Rendered).
158 // --- Normal category in no total category (Rendered).
159 // -- No total category 2 (Not rendered).
160 // --- Category with no total in no total category (Not rendered).
161 $gradetree = \grade_category::fetch_course_tree($course->id);
162 foreach ($gradetree['children'] as $child) {
163 if ($child['object']->fullname == $nototalcategory2) {
164 $this->assertFalse(\grade_tree::can_output_item($child));
165 } else {
166 $this->assertTrue(\grade_tree::can_output_item($child));
168 if (!empty($child['children'])) {
169 foreach ($child['children'] as $grandchild) {
170 if ($grandchild['object']->fullname == $nototalinnototalcategory) {
171 $this->assertFalse(\grade_tree::can_output_item($grandchild));
172 } else {
173 $this->assertTrue(\grade_tree::can_output_item($grandchild));
181 * Tests that ungraded_counts calculates count and sum of grades correctly when there are graded users.
183 * @covers \grade_report::ungraded_counts
185 public function test_ungraded_counts_count_sumgrades() {
186 global $DB;
188 $this->resetAfterTest(true);
190 $course1 = $this->getDataGenerator()->create_course();
191 $course2 = $this->getDataGenerator()->create_course();
193 $studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
194 $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher'], '*', MUST_EXIST);
196 // Custom roles (gradable and non gradable).
197 $gradeblerole = create_role('New student role', 'gradable',
198 'Gradable role', 'student');
199 $nongradeblerole = create_role('New student role', 'nongradable',
200 'Non gradable role', 'student');
202 // Set up gradable roles.
203 set_config('gradebookroles', $studentrole->id . ',' . $gradeblerole);
205 // Create users.
207 // These will be gradable users.
208 $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
209 $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
210 $student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
211 $student5 = $this->getDataGenerator()->create_user(['username' => 'student5']);
213 // These will be non-gradable users.
214 $student4 = $this->getDataGenerator()->create_user(['username' => 'student4']);
215 $student6 = $this->getDataGenerator()->create_user(['username' => 'student6']);
216 $teacher = $this->getDataGenerator()->create_user(['username' => 'teacher']);
218 // Enrol students.
219 $this->getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
220 $this->getDataGenerator()->enrol_user($student2->id, $course1->id, $studentrole->id);
221 $this->getDataGenerator()->enrol_user($student3->id, $course1->id, $gradeblerole);
223 $this->getDataGenerator()->enrol_user($student5->id, $course1->id, $nongradeblerole);
224 $this->getDataGenerator()->enrol_user($student6->id, $course1->id, $studentrole->id);
225 $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, $teacherrole->id);
227 // User that is enrolled in a different course.
228 $this->getDataGenerator()->enrol_user($student4->id, $course2->id, $studentrole->id);
230 // Mark user as deleted.
231 $student6->deleted = 1;
232 $DB->update_record('user', $student6);
234 // Create grade items in course 1.
235 $assign1 = $this->getDataGenerator()->create_module('assign', ['course' => $course1->id]);
236 $assign2 = $this->getDataGenerator()->create_module('assign', ['course' => $course1->id]);
237 $quiz1 = $this->getDataGenerator()->create_module('quiz', ['course' => $course1->id]);
239 $manuaitem = new \grade_item($this->getDataGenerator()->create_grade_item([
240 'itemname' => 'Grade item1',
241 'idnumber' => 'git1',
242 'courseid' => $course1->id,
243 ]));
245 // Create grade items in course 2.
246 $assign3 = $this->getDataGenerator()->create_module('assign', ['course' => $course2->id]);
248 // Grade users in first course.
249 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign1->id));
250 $assigninstance = new assign($cm->context, $cm, $course1);
251 $grade = $assigninstance->get_user_grade($student1->id, true);
252 $grade->grade = 40;
253 $assigninstance->update_grade($grade);
255 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign2->id));
256 $assigninstance = new assign($cm->context, $cm, $course1);
257 $grade = $assigninstance->get_user_grade($student3->id, true);
258 $grade->grade = 50;
259 $assigninstance->update_grade($grade);
261 // Override grade for assignment in gradebook.
262 $gi = \grade_item::fetch([
263 'itemtype' => 'mod',
264 'itemmodule' => 'assign',
265 'iteminstance' => $cm->instance,
266 'courseid' => $course1->id
268 $gi->update_final_grade($student3->id, 55);
270 // Grade user in second course.
271 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign3->id));
272 $assigninstance = new assign($cm->context, $cm, $course2);
273 $grade = $assigninstance->get_user_grade($student4->id, true);
274 $grade->grade = 40;
275 $assigninstance->update_grade($grade);
277 $manuaitem->update_final_grade($student1->id, 1);
278 $manuaitem->update_final_grade($student3->id, 2);
280 // Trigger a regrade.
281 grade_force_full_regrading($course1->id);
282 grade_force_full_regrading($course2->id);
283 grade_regrade_final_grades($course1->id);
284 grade_regrade_final_grades($course2->id);
286 // Initialise reports.
287 $context1 = \context_course::instance($course1->id);
288 $context2 = \context_course::instance($course2->id);
290 $gpr1 = new grade_plugin_return(
292 'type' => 'report',
293 'plugin' => 'grader',
294 'course' => $course1,
298 $gpr2 = new grade_plugin_return(
300 'type' => 'report',
301 'plugin' => 'grader',
302 'course' => $course2,
306 $report1 = new grade_report_grader($course1->id, $gpr1, $context1);
307 $report2 = new grade_report_grader($course2->id, $gpr2, $context2);
309 $ungradedcounts = [];
310 $ungradedcounts[$course1->id] = $report1->ungraded_counts(false);
311 $ungradedcounts[$course2->id] = $report2->ungraded_counts(false);
313 foreach ($ungradedcounts as $key => $ungradedcount) {
314 $gradeitems = grade_item::fetch_all(['courseid' => $key]);
315 if ($key == $course1->id) {
316 $gradeitemkeys = array_keys($gradeitems);
317 $ungradedcountskeys = array_keys($ungradedcount['ungradedcounts']);
319 // For each grade item there is some student that is not graded yet in course 1.
320 $this->assertEmpty(array_diff_key($gradeitemkeys, $ungradedcountskeys));
322 // Only quiz does not have any grades, the remaning 4 grade items should have some.
323 // We can do more and match by gradeitem id numbers. But feels like overengeneering.
324 $this->assertEquals(4, count($ungradedcount['sumarray']));
325 } else {
327 // In course 2 there is one student, and he is graded.
328 $this->assertEmpty($ungradedcount['ungradedcounts']);
330 // There are 2 grade items and they both have some grades.
331 $this->assertEquals(2, count($ungradedcount['sumarray']));
334 foreach ($gradeitems as $gradeitem) {
335 $sumgrades = null;
336 if (array_key_exists($gradeitem->id, $ungradedcount['ungradedcounts'])) {
337 $ungradeditem = $ungradedcount['ungradedcounts'][$gradeitem->id];
338 if ($gradeitem->itemtype === 'course') {
339 $this->assertEquals(1, $ungradeditem->count);
340 } else if ($gradeitem->itemmodule === 'assign') {
341 $this->assertEquals(2, $ungradeditem->count);
342 } else if ($gradeitem->itemmodule === 'quiz') {
343 $this->assertEquals(3, $ungradeditem->count);
344 } else if ($gradeitem->itemtype === 'manual') {
345 $this->assertEquals(1, $ungradeditem->count);
349 if (array_key_exists($gradeitem->id, $ungradedcount['sumarray'])) {
350 $sumgrades = $ungradedcount['sumarray'][$gradeitem->id];
351 if ($gradeitem->itemtype === 'course') {
352 if ($key == $course1->id) {
353 $this->assertEquals('98.00000', $sumgrades); // 40 + 55 + 1 + 2
354 } else {
355 $this->assertEquals('40.00000', $sumgrades);
357 } else if ($gradeitem->itemmodule === 'assign') {
358 if (($gradeitem->itemname === $assign1->name) || ($gradeitem->itemname === $assign3->name)) {
359 $this->assertEquals('40.00000', $sumgrades);
360 } else {
361 $this->assertEquals('55.00000', $sumgrades);
363 } else if ($gradeitem->itemtype === 'manual') {
364 $this->assertEquals('3.00000', $sumgrades);
372 * Tests that ungraded_counts calculates count and sum of grades correctly when there are hidden grades.
373 * @dataProvider ungraded_counts_hidden_grades_data()
374 * @param bool $hidden Whether to inlcude hidden grades or not.
375 * @param array $expectedcount expected count value (i.e. number of ugraded grades)
376 * @param array $expectedsumarray expceted sum of grades
378 * @covers \grade_report::ungraded_counts
380 public function test_ungraded_counts_hidden_grades(bool $hidden, array $expectedcount, array $expectedsumarray) {
381 $this->resetAfterTest();
383 $course = $this->getDataGenerator()->create_course();
385 // Create users.
386 $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
387 $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
388 $student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
390 // Enrol students.
391 $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
392 $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
393 $this->getDataGenerator()->enrol_user($student3->id, $course->id, 'student');
395 // Create grade items in course.
396 $manuaitem = new \grade_item($this->getDataGenerator()->create_grade_item([
397 'itemname' => 'Grade item1',
398 'idnumber' => 'git1',
399 'courseid' => $course->id,
400 ]));
402 // Grade users.
403 $manuaitem->update_final_grade($student1->id, 1);
404 $manuaitem->update_final_grade($student3->id, 2);
406 // Create a hidden grade.
407 $manuaitem->update_final_grade($student2->id, 3);
408 $grade = \grade_grade::fetch(['itemid' => $manuaitem->id, 'userid' => $student2->id]);
409 $grade->hidden = 1;
410 $grade->update();
412 // Trigger a regrade.
413 grade_force_full_regrading($course->id);
414 grade_regrade_final_grades($course->id);
416 // Initialise reports.
417 $context = \context_course::instance($course->id);
419 $gpr = new grade_plugin_return(
421 'type' => 'report',
422 'plugin' => 'grader',
423 'course' => $course,
427 $report = new grade_report_grader($course->id, $gpr, $context);
429 $ungradedcounts = $report->ungraded_counts(false, $hidden);
431 $gradeitems = grade_item::fetch_all(['courseid' => $course->id]);
433 foreach ($gradeitems as $gradeitem) {
434 $sumgrades = null;
435 if (array_key_exists($gradeitem->id, $ungradedcounts['ungradedcounts'])) {
436 $ungradeditem = $ungradedcounts['ungradedcounts'][$gradeitem->id];
437 if ($gradeitem->itemtype === 'course') {
438 $this->assertEquals($expectedcount['course'], $ungradeditem->count);
439 } else if ($gradeitem->itemtype === 'manual') {
440 $this->assertEquals($expectedcount['Grade item1'], $ungradeditem->count);
444 if (array_key_exists($gradeitem->id, $ungradedcounts['sumarray'])) {
445 $sumgrades = $ungradedcounts['sumarray'][$gradeitem->id];
446 if ($gradeitem->itemtype === 'course') {
447 $this->assertEquals($expectedsumarray['course'], $sumgrades);
448 } else if ($gradeitem->itemtype === 'manual') {
449 $this->assertEquals($expectedsumarray['Grade item1'], $sumgrades);
456 * Data provider for test_ungraded_counts_hidden_grades
458 * @return array of testing scenarios
460 public function ungraded_counts_hidden_grades_data() : array {
461 return [
462 'nohidden' => [
463 'hidden' => false,
464 'count' => ['course' => 1, 'Grade item1' => 1],
465 'sumarray' => ['course' => 6.00000, 'Grade item1' => 3.00000],
467 'includehidden' => [
468 'hidden' => true,
469 'count' => ['course' => 1, 'Grade item1' => 2],
470 'sumarray' => ['course' => 6.00000, 'Grade item1' => 6.00000],
476 * Tests that ungraded_counts calculates count and sum of grades correctly for groups when there are graded users.
478 * @covers \grade_report::ungraded_counts
480 public function test_ungraded_count_sumgrades_groups() {
481 global $DB;
483 $this->resetAfterTest();
485 $course = $this->getDataGenerator()->create_course();
487 $studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
489 // Create users.
490 $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
491 $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
492 $student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
494 // Enrol students.
495 $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
496 $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
497 $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id);
499 $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
500 $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
501 $this->getDataGenerator()->create_group_member(['userid' => $student1->id, 'groupid' => $group1->id]);
502 $this->getDataGenerator()->create_group_member(['userid' => $student2->id, 'groupid' => $group2->id]);
503 $this->getDataGenerator()->create_group_member(['userid' => $student3->id, 'groupid' => $group2->id]);
504 $DB->set_field('course', 'groupmode', SEPARATEGROUPS, ['id' => $course->id]);
506 // Create grade items.
507 $assign1 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
508 $assign2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
509 $quiz1 = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
511 $manuaitem = new \grade_item($this->getDataGenerator()->create_grade_item([
512 'itemname' => 'Grade item1',
513 'idnumber' => 'git1',
514 'courseid' => $course->id,
515 ]));
517 // Grade users.
518 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign1->id));
519 $assigninstance = new assign($cm->context, $cm, $course);
520 $grade = $assigninstance->get_user_grade($student1->id, true);
521 $grade->grade = 40;
522 $assigninstance->update_grade($grade);
524 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign2->id));
525 $assigninstance = new assign($cm->context, $cm, $course);
526 $grade = $assigninstance->get_user_grade($student3->id, true);
527 $grade->grade = 50;
528 $assigninstance->update_grade($grade);
530 $manuaitem->update_final_grade($student1->id, 1);
531 $manuaitem->update_final_grade($student3->id, 2);
533 // Trigger a regrade.
534 grade_force_full_regrading($course->id);
535 grade_regrade_final_grades($course->id);
537 // Initialise report.
538 $context = \context_course::instance($course->id);
540 $gpr1 = new grade_plugin_return(
542 'type' => 'report',
543 'plugin' => 'grader',
544 'course' => $course,
545 'groupid' => $group1->id,
549 $gpr2 = new grade_plugin_return(
551 'type' => 'report',
552 'plugin' => 'grader',
553 'course' => $course,
554 'groupid' => $group2->id,
558 $report1 = new grade_report_grader($course->id, $gpr1, $context);
559 $report2 = new grade_report_grader($course->id, $gpr2, $context);
561 $ungradedcounts = [];
562 $ungradedcounts[$group1->id] = $report1->ungraded_counts(true);
563 $ungradedcounts[$group2->id] = $report2->ungraded_counts(true);
565 $gradeitems = grade_item::fetch_all(['courseid' => $course->id]);
567 // In group1 there is 1 student and assign1 and quiz1 are not graded for him.
568 $this->assertEquals(2, count($ungradedcounts[$group1->id]['ungradedcounts']));
570 // In group1 manual grade item, assign1 and course total have some grades.
571 $this->assertEquals(3, count($ungradedcounts[$group1->id]['sumarray']));
573 // In group2 student2 has no grades at all so all 5 grade items should present.
574 $this->assertEquals(5, count($ungradedcounts[$group2->id]['ungradedcounts']));
576 // In group2 manual grade item, assign2 and course total have some grades.
577 $this->assertEquals(3, count($ungradedcounts[$group2->id]['sumarray']));
579 foreach ($gradeitems as $gradeitem) {
580 $sumgrades = null;
582 foreach ($ungradedcounts as $key => $ungradedcount) {
583 if (array_key_exists($gradeitem->id, $ungradedcount['ungradedcounts'])) {
584 $ungradeditem = $ungradedcount['ungradedcounts'][$gradeitem->id];
585 if ($key == $group1->id) {
586 // Both assign2 and quiz1 are not graded for student1.
587 $this->assertEquals(1, $ungradeditem->count);
588 } else {
589 if ($gradeitem->itemtype === 'course') {
590 $this->assertEquals(1, $ungradeditem->count);
591 } else if ($gradeitem->itemmodule === 'assign') {
592 if ($gradeitem->itemname === $assign1->name) {
593 // In group2 assign1 is not graded for anyone.
594 $this->assertEquals(2, $ungradeditem->count);
595 } else {
596 // In group2 assign2 is graded for student3.
597 $this->assertEquals(1, $ungradeditem->count);
599 } else if ($gradeitem->itemmodule === 'quiz') {
600 $this->assertEquals(2, $ungradeditem->count);
601 } else if ($gradeitem->itemtype === 'manual') {
602 $this->assertEquals(1, $ungradeditem->count);
607 if (array_key_exists($gradeitem->id, $ungradedcount['sumarray'])) {
608 $sumgrades = $ungradedcount['sumarray'][$gradeitem->id];
609 if ($key == $group1->id) {
610 if ($gradeitem->itemtype === 'course') {
611 $this->assertEquals('41.00000', $sumgrades);
612 } else if ($gradeitem->itemmodule === 'assign') {
613 $this->assertEquals('40.00000', $sumgrades);
614 } else if ($gradeitem->itemtype === 'manual') {
615 $this->assertEquals('1.00000', $sumgrades);
617 } else {
618 if ($gradeitem->itemtype === 'course') {
619 $this->assertEquals('52.00000', $sumgrades);
620 } else if ($gradeitem->itemmodule === 'assign') {
621 $this->assertEquals('50.00000', $sumgrades);
622 } else if ($gradeitem->itemtype === 'manual') {
623 $this->assertEquals('2.00000', $sumgrades);
632 * Tests that ungraded_counts calculates count and sum of grades correctly when there are hidden grades.
633 * @dataProvider ungraded_counts_only_active_enrol_data()
634 * @param bool $onlyactive Site setting to show only active users.
635 * @param int $hascapability Capability constant
636 * @param bool|null $showonlyactiveenrolpref Show only active user preference.
637 * @param array $expectedcount expected count value (i.e. number of ugraded grades)
638 * @param array $expectedsumarray expected sum of grades
640 * @covers \grade_report::ungraded_counts
642 public function test_ungraded_counts_only_active_enrol(bool $onlyactive,
643 int $hascapability, ?bool $showonlyactiveenrolpref, array $expectedcount, array $expectedsumarray) {
644 global $CFG, $DB;
646 $this->resetAfterTest();
648 $CFG->grade_report_showonlyactiveenrol = $onlyactive;
649 $course = $this->getDataGenerator()->create_course();
651 // Create users.
652 $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
653 $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
654 $student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
655 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
657 // Enrol students.
658 $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
659 $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
661 // Give teacher 'viewsuspendedusers' capability and set a preference to display suspended users.
662 $roleteacher = $DB->get_record('role', ['shortname' => 'teacher'], '*', MUST_EXIST);
663 $coursecontext = \context_course::instance($course->id);
664 assign_capability('moodle/course:viewsuspendedusers', $hascapability, $roleteacher->id, $coursecontext, true);
665 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrolpref, $teacher);
666 accesslib_clear_all_caches_for_unit_testing();
668 $this->setUser($teacher);
670 // Suspended student.
671 $this->getDataGenerator()->enrol_user($student3->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
673 // Create grade items in course.
674 $manuaitem = new \grade_item($this->getDataGenerator()->create_grade_item([
675 'itemname' => 'Grade item1',
676 'idnumber' => 'git1',
677 'courseid' => $course->id,
678 ]));
680 // Grade users.
681 $manuaitem->update_final_grade($student1->id, 1);
682 $manuaitem->update_final_grade($student3->id, 2);
684 // Trigger a regrade.
685 grade_force_full_regrading($course->id);
686 grade_regrade_final_grades($course->id);
688 // Initialise reports.
689 $context = \context_course::instance($course->id);
691 $gpr = new grade_plugin_return(
693 'type' => 'report',
694 'plugin' => 'grader',
695 'course' => $course,
699 $report = new grade_report_grader($course->id, $gpr, $context);
701 $showonlyactiveenrol = $report->show_only_active();
702 $ungradedcounts = $report->ungraded_counts(false, false, $showonlyactiveenrol);
704 $gradeitems = grade_item::fetch_all(['courseid' => $course->id]);
706 foreach ($gradeitems as $gradeitem) {
707 $sumgrades = null;
708 if (array_key_exists($gradeitem->id, $ungradedcounts['ungradedcounts'])) {
709 $ungradeditem = $ungradedcounts['ungradedcounts'][$gradeitem->id];
710 if ($gradeitem->itemtype === 'course') {
711 $this->assertEquals($expectedcount['course'], $ungradeditem->count);
712 } else if ($gradeitem->itemtype === 'manual') {
713 $this->assertEquals($expectedcount['Grade item1'], $ungradeditem->count);
717 if (array_key_exists($gradeitem->id, $ungradedcounts['sumarray'])) {
718 $sumgrades = $ungradedcounts['sumarray'][$gradeitem->id];
719 if ($gradeitem->itemtype === 'course') {
720 $this->assertEquals($expectedsumarray['course'], $sumgrades);
721 } else if ($gradeitem->itemtype === 'manual') {
722 $this->assertEquals($expectedsumarray['Grade item1'], $sumgrades);
729 * Data provider for test_ungraded_counts_hidden_grades
731 * @return array of testing scenarios
733 public function ungraded_counts_only_active_enrol_data(): array {
734 return [
735 'Show only active and no user preference' => [
736 'onlyactive' => true,
737 'hascapability' => 1,
738 'showonlyactiveenrolpref' => null,
739 'count' => ['course' => 1, 'Grade item1' => 1],
740 'sumarray' => ['course' => 1, 'Grade item1' => 1.00000],
742 'Show only active and user preference set to true' => [
743 'onlyactive' => true,
744 'hascapability' => 1,
745 'showonlyactiveenrolpref' => true,
746 'count' => ['course' => 1, 'Grade item1' => 1],
747 'sumarray' => ['course' => 1, 'Grade item1' => 1.00000],
749 'Show only active and user preference set to false' => [
750 'onlyactive' => true,
751 'hascapability' => 1,
752 'showonlyactiveenrolpref' => false,
753 'count' => ['course' => 1, 'Grade item1' => 1],
754 'sumarray' => ['course' => 3.00000, 'Grade item1' => 3.00000],
756 'Include suspended with capability and user preference set to true' => [
757 'onlyactive' => false,
758 'hascapability' => 1,
759 'showonlyactiveenrolpref' => true,
760 'count' => ['course' => 1, 'Grade item1' => 1],
761 'sumarray' => ['course' => 1.00000, 'Grade item1' => 1.00000],
763 'Include suspended with capability and user preference set to false' => [
764 'onlyactive' => false,
765 'hascapability' => 1,
766 'showonlyactiveenrolpref' => false,
767 'count' => ['course' => 1, 'Grade item1' => 1],
768 'sumarray' => ['course' => 3.00000, 'Grade item1' => 3.00000],
770 'Include suspended with capability and no user preference' => [
771 'onlyactive' => false,
772 'hascapability' => 1,
773 'showonlyactiveenrolpref' => null,
774 'count' => ['course' => 1, 'Grade item1' => 1],
775 'sumarray' => ['course' => 3.00000, 'Grade item1' => 3.00000],
777 'Include suspended without capability' => [
778 'onlyactive' => false,
779 'hascapability' => -1,
780 'showonlyactiveenrolpref' => null,
781 'count' => ['course' => 1, 'Grade item1' => 1],
782 'sumarray' => ['course' => 1.00000, 'Grade item1' => 1.00000],
788 * Tests for calculate_average.
789 * @dataProvider calculate_average_data()
790 * @param int $meanselection Whether to inlcude all grades or non-empty grades in aggregation.
791 * @param array $expectedmeancount expected meancount value
792 * @param array $expectedaverage expceted average value
794 * @covers \grade_report::calculate_average
796 public function test_calculate_average(int $meanselection, array $expectedmeancount, array $expectedaverage) {
797 global $DB;
799 $this->resetAfterTest(true);
801 $course = $this->getDataGenerator()->create_course();
803 $student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
804 $student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
805 $student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
807 $studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
809 // Enrol students.
810 $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
811 $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
812 $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id);
814 // Create activities.
815 $assign1 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
816 $assign2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
817 $quiz1 = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
819 // Grade users.
820 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign1->id));
821 $assigninstance = new assign($cm->context, $cm, $course);
822 $grade = $assigninstance->get_user_grade($student1->id, true);
823 $grade->grade = 40;
824 $assigninstance->update_grade($grade);
826 $grade = $assigninstance->get_user_grade($student2->id, true);
827 $grade->grade = 30;
828 $assigninstance->update_grade($grade);
830 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign2->id));
831 $assigninstance = new assign($cm->context, $cm, $course);
832 $grade = $assigninstance->get_user_grade($student3->id, true);
833 $grade->grade = 50;
834 $assigninstance->update_grade($grade);
836 $grade = $assigninstance->get_user_grade($student1->id, true);
837 $grade->grade = 100;
838 $assigninstance->update_grade($grade);
840 // Make a manual grade items.
841 $manuaitem = new \grade_item($this->getDataGenerator()->create_grade_item([
842 'itemname' => 'Grade item1',
843 'idnumber' => 'git1',
844 'courseid' => $course->id,
845 ]));
846 $manuaitem->update_final_grade($student1->id, 1);
847 $manuaitem->update_final_grade($student3->id, 2);
849 // Initialise report.
850 $context = \context_course::instance($course->id);
852 $gpr = new grade_plugin_return(
854 'type' => 'report',
855 'plugin' => 'grader',
856 'course' => $course,
860 $report = new grade_report_grader($course->id, $gpr, $context);
862 $ungradedcounts = $report->ungraded_counts(false);
863 $ungradedcounts['report']['meanselection'] = $meanselection;
865 $gradeitems = grade_item::fetch_all(['courseid' => $course->id]);
867 foreach ($gradeitems as $gradeitem) {
868 $name = $gradeitem->itemname . ' ' . $gradeitem->itemtype;
869 $aggr = $report->calculate_average($gradeitem, $ungradedcounts);
871 $this->assertEquals($expectedmeancount[$name], $aggr['meancount']);
872 $this->assertEquals($expectedaverage[$name], $aggr['average']);
877 * Data provider for test_calculate_average
879 * @return array of testing scenarios
881 public function calculate_average_data() : array {
882 return [
883 'Non-empty grades' => [
884 'meanselection' => 1,
885 'expectedmeancount' => [' course' => 3, 'Assignment 1 mod' => 2, 'Assignment 2 mod' => 2,
886 'Quiz 1 mod' => 0, 'Grade item1 manual' => 2],
887 'expectedaverage' => [' course' => 73.33333333333333, 'Assignment 1 mod' => 35.0,
888 'Assignment 2 mod' => 75.0, 'Quiz 1 mod' => null, 'Grade item1 manual' => 1.5],
890 'All grades' => [
891 'meanselection' => 0,
892 'expectedmeancount' => [' course' => 3, 'Assignment 1 mod' => 3, 'Assignment 2 mod' => 3,
893 'Quiz 1 mod' => 3, 'Grade item1 manual' => 3],
894 'expectedaverage' => [' course' => 73.33333333333333, 'Assignment 1 mod' => 23.333333333333332,
895 'Assignment 2 mod' => 50.0, 'Quiz 1 mod' => null, 'Grade item1 manual' => 1.0],
901 * Tests for item types.
903 * @covers \grade_report::item_types
905 public function test_item_types() {
906 $this->resetAfterTest(true);
908 $course1 = $this->getDataGenerator()->create_course();
909 $course2 = $this->getDataGenerator()->create_course();
911 // Create activities.
912 $this->getDataGenerator()->create_module('assign', ['course' => $course1->id]);
913 $this->getDataGenerator()->create_module('assign', ['course' => $course1->id]);
914 $this->getDataGenerator()->create_module('quiz', ['course' => $course1->id]);
916 $this->getDataGenerator()->create_module('assign', ['course' => $course2->id]);
918 // Create manual grade items.
919 new \grade_item($this->getDataGenerator()->create_grade_item([
920 'itemname' => 'Grade item1',
921 'idnumber' => 'git1',
922 'courseid' => $course1->id,
923 ]));
925 new \grade_item($this->getDataGenerator()->create_grade_item([
926 'itemname' => 'Grade item2',
927 'idnumber' => 'git2',
928 'courseid' => $course2->id,
929 ]));
931 // Create a grade category (it should not be fetched by item_types).
932 new \grade_category($this->getDataGenerator()
933 ->create_grade_category(['courseid' => $course1->id]), false);
935 // Initialise reports.
936 $context = \context_course::instance($course1->id);
938 $gpr = new grade_plugin_return(
940 'type' => 'report',
941 'plugin' => 'grader',
942 'course' => $course1,
946 $report1 = new grade_report_grader($course1->id, $gpr, $context);
948 $context = \context_course::instance($course2->id);
950 $gpr = new grade_plugin_return(
952 'type' => 'report',
953 'plugin' => 'grader',
954 'course' => $course2,
958 $report2 = new grade_report_grader($course2->id, $gpr, $context);
960 $gradeitems1 = $report1->item_types();
961 $gradeitems2 = $report2->item_types();
963 $this->assertEquals(3, count($gradeitems1));
964 $this->assertEquals(2, count($gradeitems2));
966 $this->assertArrayHasKey('assign', $gradeitems1);
967 $this->assertArrayHasKey('quiz', $gradeitems1);
968 $this->assertArrayHasKey('manual', $gradeitems1);
970 $this->assertArrayHasKey('assign', $gradeitems2);
971 $this->assertArrayHasKey('manual', $gradeitems2);
975 * Test get_gradable_users() function.
977 * @covers ::get_gradable_users
979 public function test_get_gradable_users() {
980 global $DB;
982 $this->setAdminUser();
983 $this->resetAfterTest(true);
985 $roleteacher = $DB->get_record('role', ['shortname' => 'teacher'], '*', MUST_EXIST);
987 // Create a course.
988 $course = $this->getDataGenerator()->create_course();
989 $coursecontext = \context_course::instance($course->id);
990 // Create groups.
991 $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
992 $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
993 // Create and enrol a teacher and some students into the course.
994 $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
995 $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
996 $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
997 $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
998 // Add student1 and student2 to group1.
999 $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]);
1000 $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $student2->id]);
1001 // Add student3 to group2.
1002 $this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $student3->id]);
1004 // Perform a regrade before creating the report.
1005 grade_regrade_final_grades($course->id);
1006 // Should return all gradable users (only students).
1007 $gradableusers = get_gradable_users($course->id);
1008 $this->assertEqualsCanonicalizing([$student1->id, $student2->id, $student3->id], array_keys($gradableusers));
1010 // Now, let's suspend the enrolment of student2.
1011 $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
1012 // Should return only the active gradable users (student1 and student3).
1013 $gradableusers = \grade_report::get_gradable_users($course->id);
1014 $this->assertEqualsCanonicalizing([$student1->id, $student3->id], array_keys($gradableusers));
1016 // Give teacher 'viewsuspendedusers' capability and set a preference to display suspended users.
1017 assign_capability('moodle/course:viewsuspendedusers', CAP_ALLOW, $roleteacher->id, $coursecontext, true);
1018 set_user_preference('grade_report_showonlyactiveenrol', false, $teacher);
1019 accesslib_clear_all_caches_for_unit_testing();
1021 $this->setUser($teacher);
1022 // Should return all gradable users (including suspended enrolments).
1023 $gradableusers = \grade_report::get_gradable_users($course->id);
1024 $this->assertEqualsCanonicalizing([$student1->id, $student2->id, $student3->id], array_keys($gradableusers));
1026 // Reactivate the course enrolment of student2.
1027 $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_ACTIVE);
1028 $this->setAdminUser();
1029 // Should return all gradable users from group1 (student1 and student2).
1030 $gradableusers = \grade_report::get_gradable_users($course->id, $group1->id);
1031 $this->assertEqualsCanonicalizing([$student1->id, $student2->id], array_keys($gradableusers));
1032 // Should return all gradable users from group2 (student3).
1033 $gradableusers = \grade_report::get_gradable_users($course->id, $group2->id);
1034 $this->assertEqualsCanonicalizing([$student3->id], array_keys($gradableusers));