2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
18 * @package core_grades
20 * @copyright nicolas@moodle.com
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 defined('MOODLE_INTERNAL') || die();
26 require_once(__DIR__.'/fixtures/lib.php');
29 class core_grade_grade_testcase extends grade_base_testcase {
31 public function test_grade_grade() {
32 $this->sub_test_grade_grade_construct();
33 $this->sub_test_grade_grade_insert();
34 $this->sub_test_grade_grade_update();
35 $this->sub_test_grade_grade_fetch();
36 $this->sub_test_grade_grade_fetch_all();
37 $this->sub_test_grade_grade_load_grade_item();
38 $this->sub_test_grade_grade_standardise_score();
39 $this->sub_test_grade_grade_is_locked();
40 $this->sub_test_grade_grade_set_hidden();
41 $this->sub_test_grade_grade_is_hidden();
42 $this->sub_test_grade_grade_deleted();
43 $this->sub_test_grade_grade_deleted_event();
46 protected function sub_test_grade_grade_construct() {
47 $params = new stdClass();
49 $params->itemid = $this->grade_items[0]->id;
51 $params->rawgrade = 88;
52 $params->rawgrademax = 110;
53 $params->rawgrademin = 18;
55 $grade_grade = new grade_grade($params, false);
56 $this->assertEquals($params->itemid, $grade_grade->itemid);
57 $this->assertEquals($params->rawgrade, $grade_grade->rawgrade);
60 protected function sub_test_grade_grade_insert() {
61 $grade_grade = new grade_grade();
62 $this->assertTrue(method_exists($grade_grade, 'insert'));
64 $grade_grade->itemid = $this->grade_items[0]->id;
65 $grade_grade->userid = 10;
66 $grade_grade->rawgrade = 88;
67 $grade_grade->rawgrademax = 110;
68 $grade_grade->rawgrademin = 18;
70 // Check the grade_item's needsupdate variable first.
71 $grade_grade->load_grade_item();
72 $this->assertEmpty($grade_grade->grade_item->needsupdate);
74 $grade_grade->insert();
76 $last_grade_grade = end($this->grade_grades);
78 $this->assertEquals($grade_grade->id, $last_grade_grade->id + 1);
80 // Timecreated will only be set if the grade was submitted by an activity module.
81 $this->assertTrue(empty($grade_grade->timecreated));
82 // Timemodified will only be set if the grade was submitted by an activity module.
83 $this->assertTrue(empty($grade_grade->timemodified));
85 // Keep our collection the same as is in the database.
86 $this->grade_grades[] = $grade_grade;
89 protected function sub_test_grade_grade_update() {
90 $grade_grade = new grade_grade($this->grade_grades[0], false);
91 $this->assertTrue(method_exists($grade_grade, 'update'));
94 protected function sub_test_grade_grade_fetch() {
95 $grade_grade = new grade_grade();
96 $this->assertTrue(method_exists($grade_grade, 'fetch'));
98 $grades = grade_grade::fetch(array('id'=>$this->grade_grades[0]->id));
99 $this->assertEquals($this->grade_grades[0]->id, $grades->id);
100 $this->assertEquals($this->grade_grades[0]->rawgrade, $grades->rawgrade);
103 protected function sub_test_grade_grade_fetch_all() {
104 $grade_grade = new grade_grade();
105 $this->assertTrue(method_exists($grade_grade, 'fetch_all'));
107 $grades = grade_grade::fetch_all(array());
108 $this->assertEquals(count($this->grade_grades), count($grades));
111 protected function sub_test_grade_grade_load_grade_item() {
112 $grade_grade = new grade_grade($this->grade_grades[0], false);
113 $this->assertTrue(method_exists($grade_grade, 'load_grade_item'));
114 $this->assertNull($grade_grade->grade_item);
115 $this->assertNotEmpty($grade_grade->itemid);
116 $this->assertNotNull($grade_grade->load_grade_item());
117 $this->assertNotNull($grade_grade->grade_item);
118 $this->assertEquals($this->grade_items[0]->id, $grade_grade->grade_item->id);
122 protected function sub_test_grade_grade_standardise_score() {
123 $this->assertEquals(4, round(grade_grade::standardise_score(6, 0, 7, 0, 5)));
124 $this->assertEquals(40, grade_grade::standardise_score(50, 30, 80, 0, 100));
129 * Disabling this test: the set_locked() arguments have been modified, rendering these tests useless until they are re-written
131 protected function test_grade_grade_set_locked() {
132 $grade_item = new grade_item($this->grade_items[0]);
133 $grade = new grade_grade($grade_item->get_final(1));
134 $this->assertTrue(method_exists($grade, 'set_locked'));
136 $this->assertTrue(empty($grade_item->locked));
137 $this->assertTrue(empty($grade->locked));
139 $this->assertTrue($grade->set_locked(true));
140 $this->assertFalse(empty($grade->locked));
141 $this->assertTrue($grade->set_locked(false));
142 $this->assertTrue(empty($grade->locked));
144 $this->assertTrue($grade_item->set_locked(true, true));
145 $grade = new grade_grade($grade_item->get_final(1));
147 $this->assertFalse(empty($grade->locked));
148 $this->assertFalse($grade->set_locked(true, false));
150 $this->assertTrue($grade_item->set_locked(true, false));
151 $grade = new grade_grade($grade_item->get_final(1));
153 $this->assertTrue($grade->set_locked(true, false));
157 protected function sub_test_grade_grade_is_locked() {
158 $grade = new grade_grade($this->grade_grades[0], false);
159 $this->assertTrue(method_exists($grade, 'is_locked'));
161 $this->assertFalse($grade->is_locked());
162 $grade->locked = time();
163 $this->assertTrue($grade->is_locked());
166 protected function sub_test_grade_grade_set_hidden() {
167 $grade = new grade_grade($this->grade_grades[0], false);
168 $grade_item = new grade_item($this->grade_items[0], false);
169 $this->assertTrue(method_exists($grade, 'set_hidden'));
171 $this->assertEquals(0, $grade_item->hidden);
172 $this->assertEquals(0, $grade->hidden);
174 $grade->set_hidden(0);
175 $this->assertEquals(0, $grade->hidden);
177 $grade->set_hidden(1);
178 $this->assertEquals(1, $grade->hidden);
180 $grade->set_hidden(0);
181 $this->assertEquals(0, $grade->hidden);
184 protected function sub_test_grade_grade_is_hidden() {
185 $grade = new grade_grade($this->grade_grades[0], false);
186 $this->assertTrue(method_exists($grade, 'is_hidden'));
188 $this->assertFalse($grade->is_hidden());
190 $this->assertTrue($grade->is_hidden());
192 $grade->hidden = time()-666;
193 $this->assertFalse($grade->is_hidden());
195 $grade->hidden = time()+666;
196 $this->assertTrue($grade->is_hidden());
199 public function test_flatten_dependencies() {
200 // First test a simple normal case.
201 $a = array(1 => array(2, 3), 2 => array(), 3 => array(4), 4 => array());
203 $expecteda = array(1 => array(2, 3, 4), 2 => array(), 3 => array(4), 4 => array());
204 $expectedb = array(1 => 1);
206 test_grade_grade_flatten_dependencies_array::test_flatten_dependencies_array($a, $b);
207 $this->assertSame($expecteda, $a);
208 $this->assertSame($expectedb, $b);
210 // Edge case - empty arrays.
211 $a = $b = $expecteda = $expectedb = array();
213 test_grade_grade_flatten_dependencies_array::test_flatten_dependencies_array($a, $b);
214 $this->assertSame($expecteda, $a);
215 $this->assertSame($expectedb, $b);
217 // Circular dependency.
218 $a = array(1 => array(2), 2 => array(3), 3 => array(1));
220 $expecteda = array(1 => array(1, 2, 3), 2 => array(1, 2, 3), 3 => array(1, 2, 3));
222 test_grade_grade_flatten_dependencies_array::test_flatten_dependencies_array($a, $b);
223 $this->assertSame($expecteda, $a);
224 // Note - we don't test the depth when we got circular dependencies - the main thing we wanted to test was that there was
225 // no ka-boom. The result would be hard to understand and doesn't matter.
227 // Circular dependency 2.
228 $a = array(1 => array(2), 2 => array(3), 3 => array(4), 4 => array(2, 1));
230 $expecteda = array(1 => array(1, 2, 3, 4), 2 => array(1, 2, 3, 4), 3 => array(1, 2, 3, 4), 4 => array(1, 2, 3, 4));
232 test_grade_grade_flatten_dependencies_array::test_flatten_dependencies_array($a, $b);
233 $this->assertSame($expecteda, $a);
236 public function test_grade_grade_min_max() {
238 $initialminmaxtouse = $CFG->grade_minmaxtouse;
240 $this->setAdminUser();
241 $course = $this->getDataGenerator()->create_course();
242 $user = $this->getDataGenerator()->create_user();
243 $assignrecord = $this->getDataGenerator()->create_module('assign', array('course' => $course, 'grade' => 100));
244 $cm = get_coursemodule_from_instance('assign', $assignrecord->id);
245 $assigncontext = context_module::instance($cm->id);
246 $assign = new assign($assigncontext, $cm, $course);
248 // Fetch the assignment item.
249 $giparams = array('itemtype' => 'mod', 'itemmodule' => 'assign', 'iteminstance' => $assignrecord->id,
250 'courseid' => $course->id, 'itemnumber' => 0);
251 $gi = grade_item::fetch($giparams);
252 $this->assertEquals(0, $gi->grademin);
253 $this->assertEquals(100, $gi->grademax);
255 // Give a grade to the student.
256 $usergrade = $assign->get_user_grade($user->id, true);
257 $usergrade->grade = 10;
258 $assign->update_grade($usergrade);
260 // Check the grade stored in gradebook.
261 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
262 $this->assertEquals(10, $gg->rawgrade);
263 $this->assertEquals(0, $gg->get_grade_min());
264 $this->assertEquals(100, $gg->get_grade_max());
266 // Change the min/max grade of the item.
271 // Fetch the updated item.
272 $gi = grade_item::fetch($giparams);
274 // Now check the grade grade min/max with system setting.
275 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
276 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting.
278 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
279 $this->assertEquals(2, $gg->get_grade_min());
280 $this->assertEquals(50, $gg->get_grade_max());
282 // Now with other system setting.
283 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
284 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting, and reset static cache.
285 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
286 $this->assertEquals(0, $gg->get_grade_min());
287 $this->assertEquals(100, $gg->get_grade_max());
289 // Now with overriden setting in course.
290 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
291 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_GRADE);
292 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
293 $this->assertEquals(0, $gg->get_grade_min());
294 $this->assertEquals(100, $gg->get_grade_max());
296 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
297 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_ITEM);
298 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
299 $this->assertEquals(2, $gg->get_grade_min());
300 $this->assertEquals(50, $gg->get_grade_max());
302 $CFG->grade_minmaxtouse = $initialminmaxtouse;
305 public function test_grade_grade_min_max_with_course_item() {
307 $initialminmaxtouse = $CFG->grade_minmaxtouse;
309 $this->setAdminUser();
310 $course = $this->getDataGenerator()->create_course();
311 $user = $this->getDataGenerator()->create_user();
312 $gi = grade_item::fetch_course_item($course->id);
314 // Fetch the category item.
315 $this->assertEquals(0, $gi->grademin);
316 $this->assertEquals(100, $gi->grademax);
318 // Give a grade to the student.
319 $gi->update_final_grade($user->id, 10);
321 // Check the grade min/max stored in gradebook.
322 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
323 $this->assertEquals(0, $gg->get_grade_min());
324 $this->assertEquals(100, $gg->get_grade_max());
326 // Change the min/max grade of the item.
331 // Fetch the updated item.
332 $gi = grade_item::fetch_course_item($course->id);
334 // Now check the grade grade min/max with system setting.
335 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
336 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting.
338 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
339 $this->assertEquals(0, $gg->get_grade_min());
340 $this->assertEquals(100, $gg->get_grade_max());
342 // Now with other system setting.
343 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
344 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting, and reset static cache.
345 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
346 $this->assertEquals(0, $gg->get_grade_min());
347 $this->assertEquals(100, $gg->get_grade_max());
349 // Now with overriden setting in course.
350 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
351 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_GRADE);
352 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
353 $this->assertEquals(0, $gg->get_grade_min());
354 $this->assertEquals(100, $gg->get_grade_max());
356 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
357 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_ITEM);
358 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
359 $this->assertEquals(0, $gg->get_grade_min());
360 $this->assertEquals(100, $gg->get_grade_max());
362 $CFG->grade_minmaxtouse = $initialminmaxtouse;
365 public function test_grade_grade_min_max_with_category_item() {
367 $initialminmaxtouse = $CFG->grade_minmaxtouse;
369 $this->setAdminUser();
370 $course = $this->getDataGenerator()->create_course();
371 $user = $this->getDataGenerator()->create_user();
372 $coursegi = grade_item::fetch_course_item($course->id);
374 // Create a category item.
375 $gc = new grade_category(array('courseid' => $course->id, 'fullname' => 'test'), false);
377 $gi = $gc->get_grade_item();
382 // Fetch the category item.
383 $giparams = array('itemtype' => 'category', 'iteminstance' => $gc->id);
384 $gi = grade_item::fetch($giparams);
385 $this->assertEquals(0, $gi->grademin);
386 $this->assertEquals(100, $gi->grademax);
388 // Give a grade to the student.
389 $gi->update_final_grade($user->id, 10);
391 // Check the grade min/max stored in gradebook.
392 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
393 $this->assertEquals(0, $gg->get_grade_min());
394 $this->assertEquals(100, $gg->get_grade_max());
396 // Change the min/max grade of the item.
401 // Fetch the updated item.
402 $gi = grade_item::fetch($giparams);
404 // Now check the grade grade min/max with system setting.
405 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
406 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting.
408 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
409 $this->assertEquals(0, $gg->get_grade_min());
410 $this->assertEquals(100, $gg->get_grade_max());
412 // Now with other system setting.
413 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
414 grade_set_setting($course->id, 'minmaxtouse', null); // Ensure no course setting, and reset static cache.
415 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
416 $this->assertEquals(0, $gg->get_grade_min());
417 $this->assertEquals(100, $gg->get_grade_max());
419 // Now with overriden setting in course.
420 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_ITEM;
421 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_GRADE);
422 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
423 $this->assertEquals(0, $gg->get_grade_min());
424 $this->assertEquals(100, $gg->get_grade_max());
426 $CFG->grade_minmaxtouse = GRADE_MIN_MAX_FROM_GRADE_GRADE;
427 grade_set_setting($course->id, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_ITEM);
428 $gg = grade_grade::fetch(array('userid' => $user->id, 'itemid' => $gi->id));
429 $this->assertEquals(0, $gg->get_grade_min());
430 $this->assertEquals(100, $gg->get_grade_max());
432 $CFG->grade_minmaxtouse = $initialminmaxtouse;
436 * Tests when a grade_grade has been deleted.
438 public function sub_test_grade_grade_deleted() {
439 $dg = $this->getDataGenerator();
441 // Create the data we need for the tests.
442 $fs = new file_storage();
443 $u1 = $dg->create_user();
444 $c1 = $dg->create_course();
445 $a1 = $dg->create_module('assign', ['course' => $c1->id]);
446 $a1context = context_module::instance($a1->cmid);
448 $gi = new grade_item($dg->create_grade_item(
450 'courseid' => $c1->id,
452 'itemmodule' => 'assign',
453 'iteminstance' => $a1->id
457 // Add feedback files to copy as our update.
458 $this->add_feedback_file_to_copy();
460 $grades['feedback'] = 'Nice feedback!';
461 $grades['feedbackformat'] = FORMAT_MOODLE;
462 $grades['feedbackfiles'] = [
464 'component' => 'test',
465 'filearea' => 'testarea',
469 $grades['userid'] = $u1->id;
470 grade_update('mod/assign', $gi->courseid, $gi->itemtype, $gi->itemmodule, $gi->iteminstance,
471 $gi->itemnumber, $grades);
473 // Feedback file area.
474 $files = $fs->get_area_files($a1context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA);
475 $this->assertEquals(2, count($files));
477 // History file area.
478 $files = $fs->get_area_files($a1context->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
479 $this->assertEquals(2, count($files));
481 $gg = grade_grade::fetch(array('userid' => $u1->id, 'itemid' => $gi->id));
485 // Feedback file area.
486 $files = $fs->get_area_files($a1context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA);
487 $this->assertEquals(0, count($files));
489 // History file area.
490 $files = $fs->get_area_files($a1context->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
491 $this->assertEquals(2, count($files));
495 * Creates a feedback file to copy to the gradebook area.
497 private function add_feedback_file_to_copy() {
500 'component' => 'test',
501 'filearea' => 'testarea',
504 'filename' => 'feedback1.txt'
507 $fs = get_file_storage();
508 $fs->create_file_from_string($dummy, '');
512 * Tests grade_deleted event.
514 public function sub_test_grade_grade_deleted_event() {
516 $dg = $this->getDataGenerator();
518 // Create the data we need for the tests.
519 $u1 = $dg->create_user();
520 $u2 = $dg->create_user();
521 $c1 = $dg->create_course();
522 $a1 = $dg->create_module('assign', ['course' => $c1->id]);
524 $gi = new grade_item($dg->create_grade_item(
526 'courseid' => $c1->id,
528 'itemmodule' => 'assign',
529 'iteminstance' => $a1->id
533 grade_update('mod/assign', $gi->courseid, $gi->itemtype, $gi->itemmodule, $gi->iteminstance,
534 $gi->itemnumber, ['userid' => $u1->id]);
535 grade_update('mod/assign', $gi->courseid, $gi->itemtype, $gi->itemmodule, $gi->iteminstance,
536 $gi->itemnumber, ['userid' => $u2->id]);
538 $gg = grade_grade::fetch(array('userid' => $u1->id, 'itemid' => $gi->id));
539 $this->assertEquals($u1->id, $gg->userid);
540 $gg->load_grade_item();
541 $this->assertEquals($gi->id, $gg->grade_item->id);
543 // Delete user with valid grade item.
544 $sink = $this->redirectEvents();
545 grade_user_delete($u1->id);
546 $events = $sink->get_events();
547 $event = reset($events);
549 $this->assertInstanceOf('core\event\grade_deleted', $event);
551 $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
552 $this->assertEquals($u2->id, $gg->userid);
553 $gg->load_grade_item();
554 $this->assertEquals($gi->id, $gg->grade_item->id);
556 // Delete grade item, mock up orphaned grade_grades.
557 $DB->delete_records('grade_items', ['id' => $gi->id]);
558 $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
559 $this->assertEquals($u2->id, $gg->userid);
561 // No event is triggered and there is a debugging message.
562 $sink = $this->redirectEvents();
563 grade_user_delete($u2->id);
564 $this->assertDebuggingCalled("Missing grade item id $gi->id");
565 $events = $sink->get_events();
567 $this->assertEmpty($events);
569 // The grade should be deleted.
570 $gg = grade_grade::fetch(array('userid' => $u2->id, 'itemid' => $gi->id));
571 $this->assertEmpty($gg);