Merge branch 'MDL-75553-311' of https://github.com/junpataleta/moodle into MOODLE_311...
[moodle.git] / lib / tests / completionlib_test.php
blob1852747d29ed2390228ef393e7f61f1eca2dc42a
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 * Completion tests.
20 * @package core_completion
21 * @category phpunit
22 * @copyright 2008 Sam Marshall
23 * @copyright 2013 Frédéric Massart
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir.'/completionlib.php');
32 /**
33 * Completion tests.
35 * @package core_completion
36 * @category phpunit
37 * @copyright 2008 Sam Marshall
38 * @copyright 2013 Frédéric Massart
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 * @coversDefaultClass \completion_info
42 class completionlib_test extends advanced_testcase {
43 protected $course;
44 protected $user;
45 protected $module1;
46 protected $module2;
48 protected function mock_setup() {
49 global $DB, $CFG, $USER;
51 $this->resetAfterTest();
53 $DB = $this->createMock(get_class($DB));
54 $CFG->enablecompletion = COMPLETION_ENABLED;
55 $USER = (object)array('id' => 314159);
58 /**
59 * Create course with user and activities.
61 protected function setup_data() {
62 global $DB, $CFG;
64 $this->resetAfterTest();
66 // Enable completion before creating modules, otherwise the completion data is not written in DB.
67 $CFG->enablecompletion = true;
69 // Create a course with activities.
70 $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
71 $this->user = $this->getDataGenerator()->create_user();
72 $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
74 $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
75 $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
78 /**
79 * Asserts that two variables are equal.
81 * @param mixed $expected
82 * @param mixed $actual
83 * @param string $message
84 * @param float $delta
85 * @param integer $maxDepth
86 * @param boolean $canonicalize
87 * @param boolean $ignoreCase
89 public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
90 bool $canonicalize = false, bool $ignoreCase = false): void {
91 // Nasty cheating hack: prevent random failures on timemodified field.
92 if (is_array($actual) && (is_object($expected) || is_array($expected))) {
93 $actual = (object) $actual;
94 $expected = (object) $expected;
96 if (is_object($expected) and is_object($actual)) {
97 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
98 if ($expected->timemodified + 1 == $actual->timemodified) {
99 $expected = clone($expected);
100 $expected->timemodified = $actual->timemodified;
104 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
108 * @covers ::is_enabled_for_site
109 * @covers ::is_enabled
111 public function test_is_enabled() {
112 global $CFG;
113 $this->mock_setup();
115 // Config alone.
116 $CFG->enablecompletion = COMPLETION_DISABLED;
117 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
118 $CFG->enablecompletion = COMPLETION_ENABLED;
119 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
121 // Course.
122 $course = (object)array('id' => 13);
123 $c = new completion_info($course);
124 $course->enablecompletion = COMPLETION_DISABLED;
125 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
126 $course->enablecompletion = COMPLETION_ENABLED;
127 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
128 $CFG->enablecompletion = COMPLETION_DISABLED;
129 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
131 // Course and CM.
132 $cm = new stdClass();
133 $cm->completion = COMPLETION_TRACKING_MANUAL;
134 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
135 $CFG->enablecompletion = COMPLETION_ENABLED;
136 $course->enablecompletion = COMPLETION_DISABLED;
137 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
138 $course->enablecompletion = COMPLETION_ENABLED;
139 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
140 $cm->completion = COMPLETION_TRACKING_NONE;
141 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
142 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
143 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
147 * @covers ::update_state
149 public function test_update_state() {
150 $this->mock_setup();
152 $mockbuilder = $this->getMockBuilder('completion_info');
153 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
154 'user_can_override_completion'));
155 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
156 $cm = (object)array('id' => 13, 'course' => 42);
158 // Not enabled, should do nothing.
159 $c = $mockbuilder->getMock();
160 $c->expects($this->once())
161 ->method('is_enabled')
162 ->with($cm)
163 ->will($this->returnValue(false));
164 $c->update_state($cm);
166 // Enabled, but current state is same as possible result, do nothing.
167 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
168 $c = $mockbuilder->getMock();
169 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
170 $c->expects($this->once())
171 ->method('is_enabled')
172 ->with($cm)
173 ->will($this->returnValue(true));
174 $c->expects($this->once())
175 ->method('get_data')
176 ->will($this->returnValue($current));
177 $c->update_state($cm, COMPLETION_COMPLETE);
179 // Enabled, but current state is a specific one and new state is just
180 // complete, so do nothing.
181 $c = $mockbuilder->getMock();
182 $current->completionstate = COMPLETION_COMPLETE_PASS;
183 $c->expects($this->once())
184 ->method('is_enabled')
185 ->with($cm)
186 ->will($this->returnValue(true));
187 $c->expects($this->once())
188 ->method('get_data')
189 ->will($this->returnValue($current));
190 $c->update_state($cm, COMPLETION_COMPLETE);
192 // Manual, change state (no change).
193 $c = $mockbuilder->getMock();
194 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
195 $current->completionstate = COMPLETION_COMPLETE;
196 $c->expects($this->once())
197 ->method('is_enabled')
198 ->with($cm)
199 ->will($this->returnValue(true));
200 $c->expects($this->once())
201 ->method('get_data')
202 ->will($this->returnValue($current));
203 $c->update_state($cm, COMPLETION_COMPLETE);
205 // Manual, change state (change).
206 $c = $mockbuilder->getMock();
207 $c->expects($this->once())
208 ->method('is_enabled')
209 ->with($cm)
210 ->will($this->returnValue(true));
211 $c->expects($this->once())
212 ->method('get_data')
213 ->will($this->returnValue($current));
214 $changed = clone($current);
215 $changed->timemodified = time();
216 $changed->completionstate = COMPLETION_INCOMPLETE;
217 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
218 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
219 $c->expects($this->once())
220 ->method('internal_set_data')
221 ->with($cm, $comparewith);
222 $c->update_state($cm, COMPLETION_INCOMPLETE);
224 // Auto, change state.
225 $c = $mockbuilder->getMock();
226 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
227 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
228 $c->expects($this->once())
229 ->method('is_enabled')
230 ->with($cm)
231 ->will($this->returnValue(true));
232 $c->expects($this->once())
233 ->method('get_data')
234 ->will($this->returnValue($current));
235 $c->expects($this->once())
236 ->method('internal_get_state')
237 ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
238 $changed = clone($current);
239 $changed->timemodified = time();
240 $changed->completionstate = COMPLETION_COMPLETE_PASS;
241 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
242 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
243 $c->expects($this->once())
244 ->method('internal_set_data')
245 ->with($cm, $comparewith);
246 $c->update_state($cm, COMPLETION_COMPLETE_PASS);
248 // Manual tracking, change state by overriding it manually.
249 $c = $mockbuilder->getMock();
250 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
251 $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
252 $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
253 $c->expects($this->exactly(2))
254 ->method('is_enabled')
255 ->with($cm)
256 ->will($this->returnValue(true));
257 $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
258 ->method('user_can_override_completion')
259 ->will($this->returnValue(true));
260 $c->expects($this->exactly(2))
261 ->method('get_data')
262 ->with($cm, false, 100)
263 ->willReturnOnConsecutiveCalls($current1, $current2);
264 $changed1 = clone($current1);
265 $changed1->timemodified = time();
266 $changed1->completionstate = COMPLETION_COMPLETE;
267 $changed1->overrideby = 314159;
268 $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
269 $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
270 $changed2 = clone($current2);
271 $changed2->timemodified = time();
272 $changed2->overrideby = null;
273 $changed2->completionstate = COMPLETION_INCOMPLETE;
274 $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
275 $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
276 $c->expects($this->exactly(2))
277 ->method('internal_set_data')
278 ->withConsecutive(
279 array($cm, $comparewith1),
280 array($cm, $comparewith2)
282 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
283 // And confirm that the status can be changed back to incomplete without an override.
284 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
286 // Auto, change state via override, incomplete to complete.
287 $c = $mockbuilder->getMock();
288 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
289 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
290 $c->expects($this->once())
291 ->method('is_enabled')
292 ->with($cm)
293 ->will($this->returnValue(true));
294 $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
295 ->method('user_can_override_completion')
296 ->will($this->returnValue(true));
297 $c->expects($this->once())
298 ->method('get_data')
299 ->with($cm, false, 100)
300 ->will($this->returnValue($current));
301 $changed = clone($current);
302 $changed->timemodified = time();
303 $changed->completionstate = COMPLETION_COMPLETE;
304 $changed->overrideby = 314159;
305 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
306 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
307 $c->expects($this->once())
308 ->method('internal_set_data')
309 ->with($cm, $comparewith);
310 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
312 // Now confirm the status can be changed back from complete to incomplete using an override.
313 $c = $mockbuilder->getMock();
314 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
315 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
316 $c->expects($this->once())
317 ->method('is_enabled')
318 ->with($cm)
319 ->will($this->returnValue(true));
320 $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
321 ->method('user_can_override_completion')
322 ->will($this->returnValue(true));
323 $c->expects($this->once())
324 ->method('get_data')
325 ->with($cm, false, 100)
326 ->will($this->returnValue($current));
327 $changed = clone($current);
328 $changed->timemodified = time();
329 $changed->completionstate = COMPLETION_INCOMPLETE;
330 $changed->overrideby = 314159;
331 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
332 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
333 $c->expects($this->once())
334 ->method('internal_set_data')
335 ->with($cm, $comparewith);
336 $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
340 * Data provider for test_internal_get_state().
342 * @return array[]
344 public function internal_get_state_provider() {
345 return [
346 'View required, but not viewed yet' => [
347 COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
349 'View not required and not viewed yet' => [
350 COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
352 'View not required, grade required but no grade yet, $cm->modname not set' => [
353 COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
355 'View not required, grade required but no grade yet, $cm->course not set' => [
356 COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
358 'View not required, grade not required' => [
359 COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
365 * Test for completion_info::get_state().
367 * @dataProvider internal_get_state_provider
368 * @param int $completionview
369 * @param int $completionusegrade
370 * @param string $unsetfield
371 * @param int $expectedstate
372 * @covers ::internal_get_state
374 public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
375 $this->setup_data();
377 /** @var \mod_assign_generator $assigngenerator */
378 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
379 $assign = $assigngenerator->create_instance([
380 'course' => $this->course->id,
381 'completion' => COMPLETION_ENABLED,
382 'completionview' => $completionview,
383 'completionusegrade' => $completionusegrade,
386 $userid = $this->user->id;
387 $this->setUser($userid);
389 $cm = get_coursemodule_from_instance('assign', $assign->id);
390 if ($unsetfield) {
391 unset($cm->$unsetfield);
393 // If view is required, but they haven't viewed it yet.
394 $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
396 $completioninfo = new completion_info($this->course);
397 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
401 * Covers the case where internal_get_state() is being called for a user different from the logged in user.
403 * @covers ::internal_get_state
405 public function test_internal_get_state_with_different_user() {
406 $this->setup_data();
408 /** @var \mod_assign_generator $assigngenerator */
409 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
410 $assign = $assigngenerator->create_instance([
411 'course' => $this->course->id,
412 'completion' => COMPLETION_ENABLED,
413 'completionusegrade' => 1,
416 $userid = $this->user->id;
418 $cm = get_coursemodule_from_instance('assign', $assign->id);
419 $usercm = cm_info::create($cm, $userid);
421 // Create a teacher account.
422 $teacher = $this->getDataGenerator()->create_user();
423 $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
424 // Log in as the teacher.
425 $this->setUser($teacher);
427 // Grade the student for this assignment.
428 $assign = new assign($usercm->context, $cm, $cm->course);
429 $data = (object)[
430 'sendstudentnotifications' => false,
431 'attemptnumber' => 1,
432 'grade' => 90,
434 $assign->save_grade($userid, $data);
436 // The target user already received a grade, so internal_get_state should be already complete.
437 $completioninfo = new completion_info($this->course);
438 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
440 // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
441 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
445 * Test for internal_get_state() for an activity that supports custom completion.
447 * @covers ::internal_get_state
449 public function test_internal_get_state_with_custom_completion() {
450 $this->setup_data();
452 $choicerecord = [
453 'course' => $this->course,
454 'completion' => COMPLETION_TRACKING_AUTOMATIC,
455 'completionsubmit' => COMPLETION_ENABLED,
457 $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
458 $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
460 $completioninfo = new completion_info($this->course);
462 // Fetch completion for the user who hasn't made a choice yet.
463 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
464 $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
466 // Have the user make a choice.
467 $choicewithoptions = choice_get_choice($choice->id);
468 $optionids = array_keys($choicewithoptions->option);
469 choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
470 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
471 $this->assertEquals(COMPLETION_COMPLETE, $completion);
475 * @covers ::set_module_viewed
477 public function test_set_module_viewed() {
478 $this->mock_setup();
480 $mockbuilder = $this->getMockBuilder('completion_info');
481 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
482 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
483 $cm = (object)array('id' => 13, 'course' => 42);
485 // Not tracking completion, should do nothing.
486 $c = $mockbuilder->getMock();
487 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
488 $c->set_module_viewed($cm);
490 // Tracking completion but completion is disabled, should do nothing.
491 $c = $mockbuilder->getMock();
492 $cm->completionview = COMPLETION_VIEW_REQUIRED;
493 $c->expects($this->once())
494 ->method('is_enabled')
495 ->with($cm)
496 ->will($this->returnValue(false));
497 $c->set_module_viewed($cm);
499 // Now it's enabled, we expect it to get data. If data already has
500 // viewed, still do nothing.
501 $c = $mockbuilder->getMock();
502 $c->expects($this->once())
503 ->method('is_enabled')
504 ->with($cm)
505 ->will($this->returnValue(true));
506 $c->expects($this->once())
507 ->method('get_data')
508 ->with($cm, 0)
509 ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
510 $c->set_module_viewed($cm);
512 // OK finally one that hasn't been viewed, now it should set it viewed
513 // and update state.
514 $c = $mockbuilder->getMock();
515 $c->expects($this->once())
516 ->method('is_enabled')
517 ->with($cm)
518 ->will($this->returnValue(true));
519 $c->expects($this->once())
520 ->method('get_data')
521 ->with($cm, false, 1337)
522 ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
523 $c->expects($this->once())
524 ->method('internal_set_data')
525 ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
526 $c->expects($this->once())
527 ->method('update_state')
528 ->with($cm, COMPLETION_COMPLETE, 1337);
529 $c->set_module_viewed($cm, 1337);
533 * @covers ::count_user_data
535 public function test_count_user_data() {
536 global $DB;
537 $this->mock_setup();
539 $course = (object)array('id' => 13);
540 $cm = (object)array('id' => 42);
542 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
543 $DB->expects($this->once())
544 ->method('get_field_sql')
545 ->will($this->returnValue(666));
547 $c = new completion_info($course);
548 $this->assertEquals(666, $c->count_user_data($cm));
552 * @covers ::delete_all_state
554 public function test_delete_all_state() {
555 global $DB;
556 $this->mock_setup();
558 $course = (object)array('id' => 13);
559 $cm = (object)array('id' => 42, 'course' => 13);
560 $c = new completion_info($course);
562 // Check it works ok without data in session.
563 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
564 $DB->expects($this->once())
565 ->method('delete_records')
566 ->with('course_modules_completion', array('coursemoduleid' => 42))
567 ->will($this->returnValue(true));
568 $c->delete_all_state($cm);
572 * @covers ::reset_all_state
574 public function test_reset_all_state() {
575 global $DB;
576 $this->mock_setup();
578 $mockbuilder = $this->getMockBuilder('completion_info');
579 $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
580 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
581 $c = $mockbuilder->getMock();
583 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
585 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
586 $DB->expects($this->once())
587 ->method('get_recordset')
588 ->will($this->returnValue(
589 new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
590 (object)array('id' => 2, 'userid' => 101)))));
592 $c->expects($this->once())
593 ->method('delete_all_state')
594 ->with($cm);
596 $c->expects($this->once())
597 ->method('get_tracked_users')
598 ->will($this->returnValue(array(
599 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
600 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
602 $c->expects($this->exactly(3))
603 ->method('update_state')
604 ->withConsecutive(
605 array($cm, COMPLETION_UNKNOWN, 100),
606 array($cm, COMPLETION_UNKNOWN, 101),
607 array($cm, COMPLETION_UNKNOWN, 201)
610 $c->reset_all_state($cm);
614 * Data provider for test_get_data().
616 * @return array[]
618 public function get_data_provider() {
619 return [
620 'No completion record' => [
621 false, true, false, COMPLETION_INCOMPLETE
623 'Not completed' => [
624 false, true, true, COMPLETION_INCOMPLETE
626 'Completed' => [
627 false, true, true, COMPLETION_COMPLETE
629 'Whole course, complete' => [
630 true, true, true, COMPLETION_COMPLETE
632 'Get data for another user, result should be not cached' => [
633 false, false, true, COMPLETION_INCOMPLETE
635 'Get data for another user, including whole course, result should be not cached' => [
636 true, false, true, COMPLETION_INCOMPLETE
642 * Tests for completion_info::get_data().
644 * @dataProvider get_data_provider
645 * @param bool $wholecourse Whole course parameter for get_data().
646 * @param bool $sameuser Whether the user calling get_data() is the user itself.
647 * @param bool $hasrecord Whether to create a course_modules_completion record.
648 * @param int $completion The completion state expected.
649 * @covers ::get_data
651 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
652 global $DB;
654 $this->setup_data();
655 $user = $this->user;
657 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
658 $choice = $choicegenerator->create_instance([
659 'course' => $this->course->id,
660 'completion' => COMPLETION_TRACKING_AUTOMATIC,
661 'completionview' => true,
662 'completionsubmit' => true,
665 $cm = get_coursemodule_from_instance('choice', $choice->id);
667 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
668 if ($hasrecord) {
669 $cmcompletionrecord = (object)[
670 'coursemoduleid' => $cm->id,
671 'userid' => $user->id,
672 'completionstate' => $completion,
673 'viewed' => 0,
674 'overrideby' => null,
675 'timemodified' => 0,
677 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
680 // Whether we expect for the returned completion data to be stored in the cache.
681 $iscached = true;
683 if (!$sameuser) {
684 $iscached = false;
685 $this->setAdminUser();
686 } else {
687 $this->setUser($user);
690 // Mock other completion data.
691 $completioninfo = new completion_info($this->course);
693 $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
695 // Course module ID of the returned completion data must match this activity's course module ID.
696 $this->assertEquals($cm->id, $result->coursemoduleid);
697 // User ID of the returned completion data must match the user's ID.
698 $this->assertEquals($user->id, $result->userid);
699 // The completion state of the returned completion data must match the expected completion state.
700 $this->assertEquals($completion, $result->completionstate);
702 // If the user has no completion record, then the default record should be returned.
703 if (!$hasrecord) {
704 $this->assertEquals(0, $result->id);
707 // Check that we are including relevant completion data for the module.
708 if (!$wholecourse) {
709 $this->assertTrue(property_exists($result, 'viewed'));
710 $this->assertTrue(property_exists($result, 'customcompletion'));
715 * @covers ::get_data
717 public function test_get_data_successive_calls(): void {
718 global $DB;
720 $this->setup_data();
721 $this->setUser($this->user);
723 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
724 $choice = $choicegenerator->create_instance([
725 'course' => $this->course->id,
726 'completion' => COMPLETION_TRACKING_AUTOMATIC,
727 'completionview' => true,
728 'completionsubmit' => true,
731 $cm = get_coursemodule_from_instance('choice', $choice->id);
733 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
734 $cmcompletionrecord = (object) [
735 'coursemoduleid' => $cm->id,
736 'userid' => $this->user->id,
737 'completionstate' => COMPLETION_NOT_VIEWED,
738 'viewed' => 0,
739 'overrideby' => null,
740 'timemodified' => 0,
742 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
744 // Mock other completion data.
745 $completioninfo = new completion_info($this->course);
747 $modinfo = get_fast_modinfo($this->course);
748 $results = [];
749 foreach ($modinfo->cms as $testcm) {
750 $result = $completioninfo->get_data($testcm, true);
751 $this->assertTrue(property_exists($result, 'id'));
752 $this->assertTrue(property_exists($result, 'coursemoduleid'));
753 $this->assertTrue(property_exists($result, 'userid'));
754 $this->assertTrue(property_exists($result, 'completionstate'));
755 $this->assertTrue(property_exists($result, 'viewed'));
756 $this->assertTrue(property_exists($result, 'overrideby'));
757 $this->assertTrue(property_exists($result, 'timemodified'));
758 $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
760 $this->assertEquals($testcm->id, $result->coursemoduleid);
761 $this->assertEquals($this->user->id, $result->userid);
762 $this->assertEquals(0, $result->viewed);
764 $results[$testcm->id] = $result;
767 $result = $completioninfo->get_data($cm);
768 $this->assertTrue(property_exists($result, 'customcompletion'));
770 // The data should match when fetching modules individually.
771 (cache::make('core', 'completion'))->purge();
772 foreach ($modinfo->cms as $testcm) {
773 $result = $completioninfo->get_data($testcm, false);
774 $this->assertEquals($result, $results[$testcm->id]);
779 * Tests for completion_info::get_other_cm_completion_data().
781 * @covers ::get_other_cm_completion_data
783 public function test_get_other_cm_completion_data() {
784 global $DB;
786 $this->setup_data();
787 $user = $this->user;
789 $this->setAdminUser();
791 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
792 $choice = $choicegenerator->create_instance([
793 'course' => $this->course->id,
794 'completion' => COMPLETION_TRACKING_AUTOMATIC,
795 'completionsubmit' => true,
798 $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
800 $choice2 = $choicegenerator->create_instance([
801 'course' => $this->course->id,
802 'completion' => COMPLETION_TRACKING_AUTOMATIC,
805 $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
807 $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
808 $workshop = $workshopgenerator->create_instance([
809 'course' => $this->course->id,
810 'completion' => COMPLETION_TRACKING_AUTOMATIC,
811 // Submission grade required.
812 'completiongradeitemnumber' => 0,
815 $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
817 $completioninfo = new completion_info($this->course);
819 $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
820 $method->setAccessible(true);
822 // Check that fetching data for a module with custom completion provides its info.
823 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
825 $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
826 $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
827 $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
829 // Mock a choice answer so user has completed the requirement.
830 $choicemockinfo = [
831 'choiceid' => $cmchoice->instance,
832 'userid' => $this->user->id
834 $DB->insert_record('choice_answers', $choicemockinfo, false);
836 // Confirm fetching again reflects the completion.
837 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
838 $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
840 // Check that fetching data for a module with no custom completion still provides its grade completion status.
841 $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
843 $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
844 $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
845 $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
847 // Check that fetching data for a module with no completion conditions does not provide any data.
848 $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
849 $this->assertEmpty($choice2completiondata);
853 * @covers ::internal_set_data
855 public function test_internal_set_data() {
856 global $DB;
857 $this->setup_data();
859 $this->setUser($this->user);
860 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
861 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
862 $cm = get_coursemodule_from_instance('forum', $forum->id);
863 $c = new completion_info($this->course);
865 // 1) Test with new data.
866 $data = new stdClass();
867 $data->id = 0;
868 $data->userid = $this->user->id;
869 $data->coursemoduleid = $cm->id;
870 $data->completionstate = COMPLETION_COMPLETE;
871 $data->timemodified = time();
872 $data->viewed = COMPLETION_NOT_VIEWED;
873 $data->overrideby = null;
875 $c->internal_set_data($cm, $data);
876 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
877 $this->assertEquals($d1, $data->id);
878 $cache = cache::make('core', 'completion');
879 // Cache was not set for another user.
880 $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
881 $this->assertEquals([
882 'cacherev' => $this->course->cacherev,
883 $cm->id => array_merge(
884 (array) $data,
885 ['other_cm_completion_data_fetched' => true]
888 $cachevalue);
890 // 2) Test with existing data and for different user.
891 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
892 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
893 $newuser = $this->getDataGenerator()->create_user();
895 $d2 = new stdClass();
896 $d2->id = 7;
897 $d2->userid = $newuser->id;
898 $d2->coursemoduleid = $cm2->id;
899 $d2->completionstate = COMPLETION_COMPLETE;
900 $d2->timemodified = time();
901 $d2->viewed = COMPLETION_NOT_VIEWED;
902 $d2->overrideby = null;
903 $c->internal_set_data($cm2, $d2);
904 // Cache for current user returns the data.
905 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
906 $this->assertEquals(array_merge(
907 (array) $data,
908 ['other_cm_completion_data_fetched' => true]
909 ), $cachevalue[$cm->id]);
911 // Cache for another user is not filled.
912 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
914 // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
915 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
916 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
917 $newuser2 = $this->getDataGenerator()->create_user();
918 $d3 = new stdClass();
919 $d3->id = 13;
920 $d3->userid = $newuser2->id;
921 $d3->coursemoduleid = $cm3->id;
922 $d3->completionstate = COMPLETION_COMPLETE;
923 $d3->timemodified = time();
924 $d3->viewed = COMPLETION_NOT_VIEWED;
925 $d3->overrideby = null;
926 $DB->insert_record('course_modules_completion', $d3);
927 $c->internal_set_data($cm, $data);
931 * @covers ::get_progress_all
933 public function test_get_progress_all_few() {
934 global $DB;
935 $this->mock_setup();
937 $mockbuilder = $this->getMockBuilder('completion_info');
938 $mockbuilder->onlyMethods(array('get_tracked_users'));
939 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
940 $c = $mockbuilder->getMock();
942 // With few results.
943 $c->expects($this->once())
944 ->method('get_tracked_users')
945 ->with(false, array(), 0, '', '', '', null)
946 ->will($this->returnValue(array(
947 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
948 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
949 $DB->expects($this->once())
950 ->method('get_in_or_equal')
951 ->with(array(100, 201))
952 ->will($this->returnValue(array(' IN (100, 201)', array())));
953 $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
954 $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
955 $DB->expects($this->once())
956 ->method('get_recordset_sql')
957 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
959 $this->assertEquals(array(
960 100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
961 'progress' => array(13 => $progress1)),
962 201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
963 'progress' => array(14 => $progress2)),
964 ), $c->get_progress_all(false));
968 * @covers ::get_progress_all
970 public function test_get_progress_all_lots() {
971 global $DB;
972 $this->mock_setup();
974 $mockbuilder = $this->getMockBuilder('completion_info');
975 $mockbuilder->onlyMethods(array('get_tracked_users'));
976 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
977 $c = $mockbuilder->getMock();
979 $tracked = array();
980 $ids = array();
981 $progress = array();
982 // With more than 1000 results.
983 for ($i = 100; $i < 2000; $i++) {
984 $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
985 $ids[] = $i;
986 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
987 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
989 $c->expects($this->once())
990 ->method('get_tracked_users')
991 ->with(true, 3, 0, '', '', '', null)
992 ->will($this->returnValue($tracked));
993 $DB->expects($this->exactly(2))
994 ->method('get_in_or_equal')
995 ->withConsecutive(
996 array(array_slice($ids, 0, 1000)),
997 array(array_slice($ids, 1000))
999 ->willReturnOnConsecutiveCalls(
1000 array(' IN whatever', array()),
1001 array(' IN whatever2', array()));
1002 $DB->expects($this->exactly(2))
1003 ->method('get_recordset_sql')
1004 ->willReturnOnConsecutiveCalls(
1005 new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1006 new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1008 $result = $c->get_progress_all(true, 3);
1009 $resultok = true;
1010 $resultok = $resultok && ($ids == array_keys($result));
1012 foreach ($result as $userid => $data) {
1013 $resultok = $resultok && $data->firstname == 'frog';
1014 $resultok = $resultok && $data->lastname == $userid;
1015 $resultok = $resultok && $data->id == $userid;
1016 $cms = $data->progress;
1017 $resultok = $resultok && (array(13, 14) == array_keys($cms));
1018 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1019 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1021 $this->assertTrue($resultok);
1022 $this->assertCount(count($tracked), $result);
1026 * @covers ::inform_grade_changed
1028 public function test_inform_grade_changed() {
1029 $this->mock_setup();
1031 $mockbuilder = $this->getMockBuilder('completion_info');
1032 $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1033 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1035 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1036 $item = (object)array('itemnumber' => 3, 'gradepass' => 1, 'hidden' => 0);
1037 $grade = (object)array('userid' => 31337, 'finalgrade' => 0, 'rawgrade' => 0);
1039 // Not enabled (should do nothing).
1040 $c = $mockbuilder->getMock();
1041 $c->expects($this->once())
1042 ->method('is_enabled')
1043 ->with($cm)
1044 ->will($this->returnValue(false));
1045 $c->inform_grade_changed($cm, $item, $grade, false);
1047 // Enabled but still no grade completion required, should still do nothing.
1048 $c = $mockbuilder->getMock();
1049 $c->expects($this->once())
1050 ->method('is_enabled')
1051 ->with($cm)
1052 ->will($this->returnValue(true));
1053 $c->inform_grade_changed($cm, $item, $grade, false);
1055 // Enabled and completion required but item number is wrong, does nothing.
1056 $c = $mockbuilder->getMock();
1057 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1058 $c->expects($this->once())
1059 ->method('is_enabled')
1060 ->with($cm)
1061 ->will($this->returnValue(true));
1062 $c->inform_grade_changed($cm, $item, $grade, false);
1064 // Enabled and completion required and item number right. It is supposed
1065 // to call update_state with the new potential state being obtained from
1066 // internal_get_grade_state.
1067 $c = $mockbuilder->getMock();
1068 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1069 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1070 $c->expects($this->once())
1071 ->method('is_enabled')
1072 ->with($cm)
1073 ->will($this->returnValue(true));
1074 $c->expects($this->once())
1075 ->method('update_state')
1076 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1077 ->will($this->returnValue(true));
1078 $c->inform_grade_changed($cm, $item, $grade, false);
1080 // Same as above but marked deleted. It is supposed to call update_state
1081 // with new potential state being COMPLETION_INCOMPLETE.
1082 $c = $mockbuilder->getMock();
1083 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1084 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1085 $c->expects($this->once())
1086 ->method('is_enabled')
1087 ->with($cm)
1088 ->will($this->returnValue(true));
1089 $c->expects($this->once())
1090 ->method('update_state')
1091 ->with($cm, COMPLETION_INCOMPLETE, 31337)
1092 ->will($this->returnValue(true));
1093 $c->inform_grade_changed($cm, $item, $grade, true);
1097 * @covers ::internal_get_grade_state
1099 public function test_internal_get_grade_state() {
1100 $this->mock_setup();
1102 $item = new stdClass;
1103 $grade = new stdClass;
1105 $item->gradepass = 4;
1106 $item->hidden = 0;
1107 $grade->rawgrade = 4.0;
1108 $grade->finalgrade = null;
1110 // Grade has pass mark and is not hidden, user passes.
1111 $this->assertEquals(
1112 COMPLETION_COMPLETE_PASS,
1113 completion_info::internal_get_grade_state($item, $grade));
1115 // Same but user fails.
1116 $grade->rawgrade = 3.9;
1117 $this->assertEquals(
1118 COMPLETION_COMPLETE_FAIL,
1119 completion_info::internal_get_grade_state($item, $grade));
1121 // User fails on raw grade but passes on final.
1122 $grade->finalgrade = 4.0;
1123 $this->assertEquals(
1124 COMPLETION_COMPLETE_PASS,
1125 completion_info::internal_get_grade_state($item, $grade));
1127 // Item is hidden.
1128 $item->hidden = 1;
1129 $this->assertEquals(
1130 COMPLETION_COMPLETE,
1131 completion_info::internal_get_grade_state($item, $grade));
1133 // Item isn't hidden but has no pass mark.
1134 $item->hidden = 0;
1135 $item->gradepass = 0;
1136 $this->assertEquals(
1137 COMPLETION_COMPLETE,
1138 completion_info::internal_get_grade_state($item, $grade));
1142 * @test ::get_activities
1144 public function test_get_activities() {
1145 global $CFG;
1146 $this->resetAfterTest();
1148 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1149 $CFG->enablecompletion = true;
1151 // Create a course with mixed auto completion data.
1152 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1153 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1154 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1155 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1156 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1157 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1158 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1160 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1161 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1162 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1164 // Create data in another course to make sure it's not considered.
1165 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1166 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1167 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1168 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1170 $c = new completion_info($course);
1171 $activities = $c->get_activities();
1172 $this->assertCount(3, $activities);
1173 $this->assertTrue(isset($activities[$forum->cmid]));
1174 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1175 $this->assertTrue(isset($activities[$page->cmid]));
1176 $this->assertSame($page->name, $activities[$page->cmid]->name);
1177 $this->assertTrue(isset($activities[$data->cmid]));
1178 $this->assertSame($data->name, $activities[$data->cmid]->name);
1180 $this->assertFalse(isset($activities[$forum2->cmid]));
1181 $this->assertFalse(isset($activities[$page2->cmid]));
1182 $this->assertFalse(isset($activities[$data2->cmid]));
1186 * @test ::has_activities
1188 public function test_has_activities() {
1189 global $CFG;
1190 $this->resetAfterTest();
1192 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1193 $CFG->enablecompletion = true;
1195 // Create a course with mixed auto completion data.
1196 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1197 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1198 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1199 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1200 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1201 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1203 $c1 = new completion_info($course);
1204 $c2 = new completion_info($course2);
1206 $this->assertTrue($c1->has_activities());
1207 $this->assertFalse($c2->has_activities());
1211 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1213 * @covers ::delete_course_completion_data
1214 * @covers ::delete_all_completion_data
1216 public function test_course_delete_prerequisite() {
1217 global $DB;
1219 $this->setup_data();
1221 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1223 $criteriadata = (object) [
1224 'id' => $this->course->id,
1225 'criteria_course' => [$courseprerequisite->id],
1228 /** @var completion_criteria_course $criteria */
1229 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1230 $criteria->update_config($criteriadata);
1232 // Sanity test.
1233 $this->assertTrue($DB->record_exists('course_completion_criteria', [
1234 'course' => $this->course->id,
1235 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1236 'courseinstance' => $courseprerequisite->id,
1237 ]));
1239 // Deleting the prerequisite course should remove the completion criteria.
1240 delete_course($courseprerequisite, false);
1242 $this->assertFalse($DB->record_exists('course_completion_criteria', [
1243 'course' => $this->course->id,
1244 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1245 'courseinstance' => $courseprerequisite->id,
1246 ]));
1250 * Test course module completion update event.
1252 * @covers \core\event\course_module_completion_updated
1254 public function test_course_module_completion_updated_event() {
1255 global $USER, $CFG;
1257 $this->setup_data();
1259 $this->setAdminUser();
1261 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1262 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1264 $c = new completion_info($this->course);
1265 $activities = $c->get_activities();
1266 $this->assertEquals(1, count($activities));
1267 $this->assertTrue(isset($activities[$forum->cmid]));
1268 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1270 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1271 $current->completionstate = COMPLETION_COMPLETE;
1272 $current->timemodified = time();
1273 $sink = $this->redirectEvents();
1274 $c->internal_set_data($activities[$forum->cmid], $current);
1275 $events = $sink->get_events();
1276 $event = reset($events);
1277 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1278 $this->assertEquals($forum->cmid,
1279 $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1280 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1281 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1282 $this->assertEquals($USER->id, $event->userid);
1283 $this->assertEquals($this->user->id, $event->relateduserid);
1284 $this->assertInstanceOf('moodle_url', $event->get_url());
1285 $this->assertEventLegacyData($current, $event);
1289 * Test course completed event.
1291 * @covers \core\event\course_completed
1293 public function test_course_completed_event() {
1294 global $USER;
1296 $this->setup_data();
1297 $this->setAdminUser();
1299 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1300 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1302 // Mark course as complete and get triggered event.
1303 $sink = $this->redirectEvents();
1304 $ccompletion->mark_complete();
1305 $events = $sink->get_events();
1306 $event = reset($events);
1308 $this->assertInstanceOf('\core\event\course_completed', $event);
1309 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1310 $this->assertEquals($this->course->id, $event->courseid);
1311 $this->assertEquals($USER->id, $event->userid);
1312 $this->assertEquals($this->user->id, $event->relateduserid);
1313 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1314 $this->assertInstanceOf('moodle_url', $event->get_url());
1315 $data = $ccompletion->get_record_data();
1316 $this->assertEventLegacyData($data, $event);
1320 * Test course completed message.
1322 * @covers \core\event\course_completed
1324 public function test_course_completed_message() {
1325 $this->setup_data();
1326 $this->setAdminUser();
1328 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1329 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1331 // Mark course as complete and get the message.
1332 $sink = $this->redirectMessages();
1333 $ccompletion->mark_complete();
1334 $messages = $sink->get_messages();
1335 $sink->close();
1337 $this->assertCount(1, $messages);
1338 $message = array_pop($messages);
1340 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1341 $this->assertEquals($this->user->id, $message->useridto);
1342 $this->assertEquals('coursecompleted', $message->eventtype);
1343 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1344 $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1348 * Test course completed event.
1350 * @covers \core\event\course_completion_updated
1352 public function test_course_completion_updated_event() {
1353 $this->setup_data();
1354 $coursecontext = context_course::instance($this->course->id);
1355 $coursecompletionevent = \core\event\course_completion_updated::create(
1356 array(
1357 'courseid' => $this->course->id,
1358 'context' => $coursecontext
1362 // Mark course as complete and get triggered event.
1363 $sink = $this->redirectEvents();
1364 $coursecompletionevent->trigger();
1365 $events = $sink->get_events();
1366 $event = array_pop($events);
1367 $sink->close();
1369 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1370 $this->assertEquals($this->course->id, $event->courseid);
1371 $this->assertEquals($coursecontext, $event->get_context());
1372 $this->assertInstanceOf('moodle_url', $event->get_url());
1373 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1374 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1378 * @covers \completion_can_view_data
1380 public function test_completion_can_view_data() {
1381 $this->setup_data();
1383 $student = $this->getDataGenerator()->create_user();
1384 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1386 $this->setUser($student);
1387 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1388 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1392 * Data provider for test_get_grade_completion().
1394 * @return array[]
1396 public function get_grade_completion_provider() {
1397 return [
1398 'Grade not required' => [false, false, null, null, null],
1399 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1400 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1401 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1402 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1407 * Test for \completion_info::get_grade_completion().
1409 * @dataProvider get_grade_completion_provider
1410 * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1411 * @param bool $hasgrade Whether to set grade for the user in this activity.
1412 * @param int|null $passinggrade Passing grade to set for the test activity.
1413 * @param string|null $expectedexception Expected exception.
1414 * @param int|null $expectedresult The expected completion status.
1415 * @covers ::get_grade_completion
1417 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
1418 ?string $expectedexception, ?int $expectedresult) {
1419 $this->setup_data();
1421 /** @var \mod_assign_generator $assigngenerator */
1422 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1423 $assign = $assigngenerator->create_instance([
1424 'course' => $this->course->id,
1425 'completion' => COMPLETION_ENABLED,
1426 'completionusegrade' => $completionusegrade,
1427 'gradepass' => $passinggrade,
1430 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1431 if ($completionusegrade && $hasgrade) {
1432 $assigninstance = new assign($cm->context, $cm, $this->course);
1433 $grade = $assigninstance->get_user_grade($this->user->id, true);
1434 $grade->grade = 75;
1435 $assigninstance->update_grade($grade);
1438 $completioninfo = new completion_info($this->course);
1439 if ($expectedexception) {
1440 $this->expectException($expectedexception);
1442 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1443 $this->assertEquals($expectedresult, $gradecompletion);
1447 * Test the return value for cases when the activity module does not have associated grade_item.
1449 * @covers ::get_grade_completion
1451 public function test_get_grade_completion_without_grade_item() {
1452 global $DB;
1454 $this->setup_data();
1456 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1457 'course' => $this->course->id,
1458 'completion' => COMPLETION_ENABLED,
1459 'completionusegrade' => true,
1460 'gradepass' => 42,
1463 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1465 $DB->delete_records('grade_items', [
1466 'courseid' => $this->course->id,
1467 'itemtype' => 'mod',
1468 'itemmodule' => 'assign',
1469 'iteminstance' => $assign->id,
1472 // Without the grade_item, the activity is considered incomplete.
1473 $completioninfo = new completion_info($this->course);
1474 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1476 // Once the activity is graded, the grade_item is automatically created.
1477 $assigninstance = new assign($cm->context, $cm, $this->course);
1478 $grade = $assigninstance->get_user_grade($this->user->id, true);
1479 $grade->grade = 40;
1480 $assigninstance->update_grade($grade);
1482 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1483 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1487 class core_completionlib_fake_recordset implements Iterator {
1488 protected $closed;
1489 protected $values, $index;
1491 public function __construct($values) {
1492 $this->values = $values;
1493 $this->index = 0;
1496 public function current() {
1497 return $this->values[$this->index];
1500 public function key() {
1501 return $this->values[$this->index];
1504 public function next() {
1505 $this->index++;
1508 public function rewind() {
1509 $this->index = 0;
1512 public function valid() {
1513 return count($this->values) > $this->index;
1516 public function close() {
1517 $this->closed = true;
1520 public function was_closed() {
1521 return $this->closed;