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/>.
20 * @package core_completion
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();
30 require_once($CFG->libdir
.'/completionlib.php');
35 * @package core_completion
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
{
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);
59 * Create course with user and activities.
61 protected function setup_data() {
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
));
79 * Asserts that two variables are equal.
81 * @param mixed $expected
82 * @param mixed $actual
83 * @param string $message
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() {
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());
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());
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() {
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')
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')
173 ->will($this->returnValue(true));
174 $c->expects($this->once())
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')
186 ->will($this->returnValue(true));
187 $c->expects($this->once())
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')
199 ->will($this->returnValue(true));
200 $c->expects($this->once())
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')
210 ->will($this->returnValue(true));
211 $c->expects($this->once())
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')
231 ->will($this->returnValue(true));
232 $c->expects($this->once())
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')
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))
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')
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')
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())
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')
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())
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().
344 public function internal_get_state_provider() {
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) {
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
);
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 * Provider for the test_internal_get_state_with_grade_criteria.
404 * @covers ::internal_get_state
406 public function test_internal_get_state_with_grade_criteria_provider() {
408 "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
410 'completionusegrade' => 1,
411 'completionpassgrade' => 1,
415 COMPLETION_COMPLETE_PASS
417 "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
419 'completionusegrade' => 1,
420 'completionpassgrade' => 1,
424 COMPLETION_COMPLETE_FAIL
426 "Passing grade not enabled with passing grade set." => [
428 'completionusegrade' => 1,
432 COMPLETION_COMPLETE_PASS
434 "Passing grade not enabled with passing grade not set." => [
436 'completionusegrade' => 1,
441 "Passing grade not enabled with passing grade not set. No submission made." => [
443 'completionusegrade' => 1,
446 COMPLETION_INCOMPLETE
452 * Tests that the right completion state is being set based on the grade criteria.
454 * @dataProvider test_internal_get_state_with_grade_criteria_provider
455 * @param array $completioncriteria The completion criteria to use
456 * @param int|null $studentgrade Grade to assign to student
457 * @param int $expectedstate Expected completion state
458 * @covers ::internal_get_state
460 public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?
int $studentgrade, int $expectedstate) {
463 /** @var \mod_assign_generator $assigngenerator */
464 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
465 $assign = $assigngenerator->create_instance([
466 'course' => $this->course
->id
,
467 'completion' => COMPLETION_ENABLED
,
468 ] +
$completioncriteria);
470 $userid = $this->user
->id
;
472 $cm = get_coursemodule_from_instance('assign', $assign->id
);
473 $usercm = cm_info
::create($cm, $userid);
475 // Create a teacher account.
476 $teacher = $this->getDataGenerator()->create_user();
477 $this->getDataGenerator()->enrol_user($teacher->id
, $this->course
->id
, 'editingteacher');
478 // Log in as the teacher.
479 $this->setUser($teacher);
481 // Grade the student for this assignment.
482 $assign = new assign($usercm->context
, $cm, $cm->course
);
485 'sendstudentnotifications' => false,
486 'attemptnumber' => 1,
487 'grade' => $studentgrade,
489 $assign->save_grade($userid, $data);
492 // The target user already received a grade, so internal_get_state should be already complete.
493 $completioninfo = new completion_info($this->course
);
494 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
498 * Covers the case where internal_get_state() is being called for a user different from the logged in user.
500 * @covers ::internal_get_state
502 public function test_internal_get_state_with_different_user() {
505 /** @var \mod_assign_generator $assigngenerator */
506 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
507 $assign = $assigngenerator->create_instance([
508 'course' => $this->course
->id
,
509 'completion' => COMPLETION_ENABLED
,
510 'completionusegrade' => 1,
513 $userid = $this->user
->id
;
515 $cm = get_coursemodule_from_instance('assign', $assign->id
);
516 $usercm = cm_info
::create($cm, $userid);
518 // Create a teacher account.
519 $teacher = $this->getDataGenerator()->create_user();
520 $this->getDataGenerator()->enrol_user($teacher->id
, $this->course
->id
, 'editingteacher');
521 // Log in as the teacher.
522 $this->setUser($teacher);
524 // Grade the student for this assignment.
525 $assign = new assign($usercm->context
, $cm, $cm->course
);
527 'sendstudentnotifications' => false,
528 'attemptnumber' => 1,
531 $assign->save_grade($userid, $data);
533 // The target user already received a grade, so internal_get_state should be already complete.
534 $completioninfo = new completion_info($this->course
);
535 $this->assertEquals(COMPLETION_COMPLETE
, $completioninfo->internal_get_state($cm, $userid, null));
537 // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
538 $this->assertEquals(COMPLETION_INCOMPLETE
, $completioninfo->internal_get_state($cm, $teacher->id
, null));
542 * Test for internal_get_state() for an activity that supports custom completion.
544 * @covers ::internal_get_state
546 public function test_internal_get_state_with_custom_completion() {
550 'course' => $this->course
,
551 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
552 'completionsubmit' => COMPLETION_ENABLED
,
554 $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
555 $cminfo = cm_info
::create(get_coursemodule_from_instance('choice', $choice->id
));
557 $completioninfo = new completion_info($this->course
);
559 // Fetch completion for the user who hasn't made a choice yet.
560 $completion = $completioninfo->internal_get_state($cminfo, $this->user
->id
, COMPLETION_INCOMPLETE
);
561 $this->assertEquals(COMPLETION_INCOMPLETE
, $completion);
563 // Have the user make a choice.
564 $choicewithoptions = choice_get_choice($choice->id
);
565 $optionids = array_keys($choicewithoptions->option
);
566 choice_user_submit_response($optionids[0], $choice, $this->user
->id
, $this->course
, $cminfo);
567 $completion = $completioninfo->internal_get_state($cminfo, $this->user
->id
, COMPLETION_INCOMPLETE
);
568 $this->assertEquals(COMPLETION_COMPLETE
, $completion);
572 * @covers ::set_module_viewed
574 public function test_set_module_viewed() {
577 $mockbuilder = $this->getMockBuilder('completion_info');
578 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
579 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
580 $cm = (object)array('id' => 13, 'course' => 42);
582 // Not tracking completion, should do nothing.
583 $c = $mockbuilder->getMock();
584 $cm->completionview
= COMPLETION_VIEW_NOT_REQUIRED
;
585 $c->set_module_viewed($cm);
587 // Tracking completion but completion is disabled, should do nothing.
588 $c = $mockbuilder->getMock();
589 $cm->completionview
= COMPLETION_VIEW_REQUIRED
;
590 $c->expects($this->once())
591 ->method('is_enabled')
593 ->will($this->returnValue(false));
594 $c->set_module_viewed($cm);
596 // Now it's enabled, we expect it to get data. If data already has
597 // viewed, still do nothing.
598 $c = $mockbuilder->getMock();
599 $c->expects($this->once())
600 ->method('is_enabled')
602 ->will($this->returnValue(true));
603 $c->expects($this->once())
606 ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED
)));
607 $c->set_module_viewed($cm);
609 // OK finally one that hasn't been viewed, now it should set it viewed
611 $c = $mockbuilder->getMock();
612 $c->expects($this->once())
613 ->method('is_enabled')
615 ->will($this->returnValue(true));
616 $c->expects($this->once())
618 ->with($cm, false, 1337)
619 ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED
)));
620 $c->expects($this->once())
621 ->method('internal_set_data')
622 ->with($cm, (object)array('viewed' => COMPLETION_VIEWED
));
623 $c->expects($this->once())
624 ->method('update_state')
625 ->with($cm, COMPLETION_COMPLETE
, 1337);
626 $c->set_module_viewed($cm, 1337);
630 * @covers ::count_user_data
632 public function test_count_user_data() {
636 $course = (object)array('id' => 13);
637 $cm = (object)array('id' => 42);
639 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
640 $DB->expects($this->once())
641 ->method('get_field_sql')
642 ->will($this->returnValue(666));
644 $c = new completion_info($course);
645 $this->assertEquals(666, $c->count_user_data($cm));
649 * @covers ::delete_all_state
651 public function test_delete_all_state() {
655 $course = (object)array('id' => 13);
656 $cm = (object)array('id' => 42, 'course' => 13);
657 $c = new completion_info($course);
659 // Check it works ok without data in session.
660 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
661 $DB->expects($this->once())
662 ->method('delete_records')
663 ->with('course_modules_completion', array('coursemoduleid' => 42))
664 ->will($this->returnValue(true));
665 $c->delete_all_state($cm);
669 * @covers ::reset_all_state
671 public function test_reset_all_state() {
675 $mockbuilder = $this->getMockBuilder('completion_info');
676 $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
677 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
678 $c = $mockbuilder->getMock();
680 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC
);
682 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
683 $DB->expects($this->once())
684 ->method('get_recordset')
685 ->will($this->returnValue(
686 new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
687 (object)array('id' => 2, 'userid' => 101)))));
689 $c->expects($this->once())
690 ->method('delete_all_state')
693 $c->expects($this->once())
694 ->method('get_tracked_users')
695 ->will($this->returnValue(array(
696 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
697 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
699 $c->expects($this->exactly(3))
700 ->method('update_state')
702 array($cm, COMPLETION_UNKNOWN
, 100),
703 array($cm, COMPLETION_UNKNOWN
, 101),
704 array($cm, COMPLETION_UNKNOWN
, 201)
707 $c->reset_all_state($cm);
711 * Data provider for test_get_data().
715 public function get_data_provider() {
717 'No completion record' => [
718 false, true, false, COMPLETION_INCOMPLETE
721 false, true, true, COMPLETION_INCOMPLETE
724 false, true, true, COMPLETION_COMPLETE
726 'Whole course, complete' => [
727 true, true, true, COMPLETION_COMPLETE
729 'Get data for another user, result should be not cached' => [
730 false, false, true, COMPLETION_INCOMPLETE
732 'Get data for another user, including whole course, result should be not cached' => [
733 true, false, true, COMPLETION_INCOMPLETE
739 * Tests for completion_info::get_data().
741 * @dataProvider get_data_provider
742 * @param bool $wholecourse Whole course parameter for get_data().
743 * @param bool $sameuser Whether the user calling get_data() is the user itself.
744 * @param bool $hasrecord Whether to create a course_modules_completion record.
745 * @param int $completion The completion state expected.
748 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
754 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
755 $choice = $choicegenerator->create_instance([
756 'course' => $this->course
->id
,
757 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
758 'completionview' => true,
759 'completionsubmit' => true,
762 $cm = get_coursemodule_from_instance('choice', $choice->id
);
764 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
766 $cmcompletionrecord = (object)[
767 'coursemoduleid' => $cm->id
,
768 'userid' => $user->id
,
769 'completionstate' => $completion,
771 'overrideby' => null,
774 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
777 // Whether we expect for the returned completion data to be stored in the cache.
782 $this->setAdminUser();
784 $this->setUser($user);
787 // Mock other completion data.
788 $completioninfo = new completion_info($this->course
);
790 $result = $completioninfo->get_data($cm, $wholecourse, $user->id
);
792 // Course module ID of the returned completion data must match this activity's course module ID.
793 $this->assertEquals($cm->id
, $result->coursemoduleid
);
794 // User ID of the returned completion data must match the user's ID.
795 $this->assertEquals($user->id
, $result->userid
);
796 // The completion state of the returned completion data must match the expected completion state.
797 $this->assertEquals($completion, $result->completionstate
);
799 // If the user has no completion record, then the default record should be returned.
801 $this->assertEquals(0, $result->id
);
804 // Check that we are including relevant completion data for the module.
806 $this->assertTrue(property_exists($result, 'viewed'));
807 $this->assertTrue(property_exists($result, 'customcompletion'));
814 public function test_get_data_successive_calls(): void
{
818 $this->setUser($this->user
);
820 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
821 $choice = $choicegenerator->create_instance([
822 'course' => $this->course
->id
,
823 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
824 'completionview' => true,
825 'completionsubmit' => true,
828 $cm = get_coursemodule_from_instance('choice', $choice->id
);
830 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
831 $cmcompletionrecord = (object) [
832 'coursemoduleid' => $cm->id
,
833 'userid' => $this->user
->id
,
834 'completionstate' => COMPLETION_NOT_VIEWED
,
836 'overrideby' => null,
839 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
841 // Mock other completion data.
842 $completioninfo = new completion_info($this->course
);
844 $modinfo = get_fast_modinfo($this->course
);
846 foreach ($modinfo->cms
as $testcm) {
847 $result = $completioninfo->get_data($testcm, true);
848 $this->assertTrue(property_exists($result, 'id'));
849 $this->assertTrue(property_exists($result, 'coursemoduleid'));
850 $this->assertTrue(property_exists($result, 'userid'));
851 $this->assertTrue(property_exists($result, 'completionstate'));
852 $this->assertTrue(property_exists($result, 'viewed'));
853 $this->assertTrue(property_exists($result, 'overrideby'));
854 $this->assertTrue(property_exists($result, 'timemodified'));
855 $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
857 $this->assertEquals($testcm->id
, $result->coursemoduleid
);
858 $this->assertEquals($this->user
->id
, $result->userid
);
859 $this->assertEquals(0, $result->viewed
);
861 $results[$testcm->id
] = $result;
864 $result = $completioninfo->get_data($cm);
865 $this->assertTrue(property_exists($result, 'customcompletion'));
867 // The data should match when fetching modules individually.
868 (cache
::make('core', 'completion'))->purge();
869 foreach ($modinfo->cms
as $testcm) {
870 $result = $completioninfo->get_data($testcm, false);
871 $this->assertEquals($result, $results[$testcm->id
]);
876 * Tests for completion_info::get_other_cm_completion_data().
878 * @covers ::get_other_cm_completion_data
880 public function test_get_other_cm_completion_data() {
886 $this->setAdminUser();
888 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
889 $choice = $choicegenerator->create_instance([
890 'course' => $this->course
->id
,
891 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
892 'completionsubmit' => true,
895 $cmchoice = cm_info
::create(get_coursemodule_from_instance('choice', $choice->id
));
897 $choice2 = $choicegenerator->create_instance([
898 'course' => $this->course
->id
,
899 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
902 $cmchoice2 = cm_info
::create(get_coursemodule_from_instance('choice', $choice2->id
));
904 $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
905 $workshop = $workshopgenerator->create_instance([
906 'course' => $this->course
->id
,
907 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
908 // Submission grade required.
909 'completiongradeitemnumber' => 0,
910 'completionpassgrade' => 1,
913 $cmworkshop = cm_info
::create(get_coursemodule_from_instance('workshop', $workshop->id
));
915 $completioninfo = new completion_info($this->course
);
917 $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
918 $method->setAccessible(true);
920 // Check that fetching data for a module with custom completion provides its info.
921 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id
);
923 $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
924 $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
925 $this->assertEquals(COMPLETION_INCOMPLETE
, $choicecompletiondata['customcompletion']['completionsubmit']);
927 // Mock a choice answer so user has completed the requirement.
929 'choiceid' => $cmchoice->instance
,
930 'userid' => $this->user
->id
932 $DB->insert_record('choice_answers', $choicemockinfo, false);
934 // Confirm fetching again reflects the completion.
935 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id
);
936 $this->assertEquals(COMPLETION_COMPLETE
, $choicecompletiondata['customcompletion']['completionsubmit']);
938 // Check that fetching data for a module with no custom completion still provides its grade completion status.
939 $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id
);
941 $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
942 $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
943 $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
944 $this->assertEquals(COMPLETION_INCOMPLETE
, $workshopcompletiondata['completiongrade']);
945 $this->assertEquals(COMPLETION_INCOMPLETE
, $workshopcompletiondata['passgrade']);
947 // Check that fetching data for a module with no completion conditions does not provide any data.
948 $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id
);
949 $this->assertEmpty($choice2completiondata);
953 * @covers ::internal_set_data
955 public function test_internal_set_data() {
959 $this->setUser($this->user
);
960 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
961 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
962 $cm = get_coursemodule_from_instance('forum', $forum->id
);
963 $c = new completion_info($this->course
);
965 // 1) Test with new data.
966 $data = new stdClass();
968 $data->userid
= $this->user
->id
;
969 $data->coursemoduleid
= $cm->id
;
970 $data->completionstate
= COMPLETION_COMPLETE
;
971 $data->timemodified
= time();
972 $data->viewed
= COMPLETION_NOT_VIEWED
;
973 $data->overrideby
= null;
975 $c->internal_set_data($cm, $data);
976 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id
));
977 $this->assertEquals($d1, $data->id
);
978 $cache = cache
::make('core', 'completion');
979 // Cache was not set for another user.
980 $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
981 $this->assertEquals([
982 'cacherev' => $this->course
->cacherev
,
983 $cm->id
=> array_merge(
985 ['other_cm_completion_data_fetched' => true]
990 // 2) Test with existing data and for different user.
991 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
992 $cm2 = get_coursemodule_from_instance('forum', $forum2->id
);
993 $newuser = $this->getDataGenerator()->create_user();
995 $d2 = new stdClass();
997 $d2->userid
= $newuser->id
;
998 $d2->coursemoduleid
= $cm2->id
;
999 $d2->completionstate
= COMPLETION_COMPLETE
;
1000 $d2->timemodified
= time();
1001 $d2->viewed
= COMPLETION_NOT_VIEWED
;
1002 $d2->overrideby
= null;
1003 $c->internal_set_data($cm2, $d2);
1004 // Cache for current user returns the data.
1005 $cachevalue = $cache->get($data->userid
. '_' . $cm->course
);
1006 $this->assertEquals(array_merge(
1008 ['other_cm_completion_data_fetched' => true]
1009 ), $cachevalue[$cm->id
]);
1011 // Cache for another user is not filled.
1012 $this->assertEquals(false, $cache->get($d2->userid
. '_' . $cm2->course
));
1014 // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1015 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
1016 $cm3 = get_coursemodule_from_instance('forum', $forum3->id
);
1017 $newuser2 = $this->getDataGenerator()->create_user();
1018 $d3 = new stdClass();
1020 $d3->userid
= $newuser2->id
;
1021 $d3->coursemoduleid
= $cm3->id
;
1022 $d3->completionstate
= COMPLETION_COMPLETE
;
1023 $d3->timemodified
= time();
1024 $d3->viewed
= COMPLETION_NOT_VIEWED
;
1025 $d3->overrideby
= null;
1026 $DB->insert_record('course_modules_completion', $d3);
1027 $c->internal_set_data($cm, $data);
1029 // 4) Test instant course completions.
1030 $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course
->id
),
1031 array('completion' => 1));
1032 $cm = get_coursemodule_from_instance('data', $dataactivity->id
);
1033 $c = new completion_info($this->course
);
1034 $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid
);
1036 // Add activity completion criteria.
1037 $criteriadata = new stdClass();
1038 $criteriadata->id
= $this->course
->id
;
1039 $criteriadata->criteria_activity
= array();
1041 $criteriadata->criteria_activity
[$cmdata->id
] = 1;
1042 $class = 'completion_criteria_activity';
1043 $criterion = new $class();
1044 $criterion->update_config($criteriadata);
1046 $actual = $DB->get_records('course_completions');
1047 $this->assertEmpty($actual);
1049 $data->coursemoduleid
= $cm->id
;
1050 $c->internal_set_data($cm, $data);
1051 $actual = $DB->get_records('course_completions');
1052 $this->assertEquals(1, count($actual));
1053 $this->assertEquals($this->user
->id
, reset($actual)->userid
);
1055 $data->userid
= $newuser2->id
;
1056 $c->internal_set_data($cm, $data, true);
1057 $actual = $DB->get_records('course_completions');
1058 $this->assertEquals(1, count($actual));
1059 $this->assertEquals($this->user
->id
, reset($actual)->userid
);
1063 * @covers ::get_progress_all
1065 public function test_get_progress_all_few() {
1067 $this->mock_setup();
1069 $mockbuilder = $this->getMockBuilder('completion_info');
1070 $mockbuilder->onlyMethods(array('get_tracked_users'));
1071 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1072 $c = $mockbuilder->getMock();
1074 // With few results.
1075 $c->expects($this->once())
1076 ->method('get_tracked_users')
1077 ->with(false, array(), 0, '', '', '', null)
1078 ->will($this->returnValue(array(
1079 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1080 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1081 $DB->expects($this->once())
1082 ->method('get_in_or_equal')
1083 ->with(array(100, 201))
1084 ->will($this->returnValue(array(' IN (100, 201)', array())));
1085 $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1086 $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1087 $DB->expects($this->once())
1088 ->method('get_recordset_sql')
1089 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1091 $this->assertEquals(array(
1092 100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1093 'progress' => array(13 => $progress1)),
1094 201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1095 'progress' => array(14 => $progress2)),
1096 ), $c->get_progress_all(false));
1100 * @covers ::get_progress_all
1102 public function test_get_progress_all_lots() {
1104 $this->mock_setup();
1106 $mockbuilder = $this->getMockBuilder('completion_info');
1107 $mockbuilder->onlyMethods(array('get_tracked_users'));
1108 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1109 $c = $mockbuilder->getMock();
1113 $progress = array();
1114 // With more than 1000 results.
1115 for ($i = 100; $i < 2000; $i++
) {
1116 $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1118 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1119 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1121 $c->expects($this->once())
1122 ->method('get_tracked_users')
1123 ->with(true, 3, 0, '', '', '', null)
1124 ->will($this->returnValue($tracked));
1125 $DB->expects($this->exactly(2))
1126 ->method('get_in_or_equal')
1128 array(array_slice($ids, 0, 1000)),
1129 array(array_slice($ids, 1000))
1131 ->willReturnOnConsecutiveCalls(
1132 array(' IN whatever', array()),
1133 array(' IN whatever2', array()));
1134 $DB->expects($this->exactly(2))
1135 ->method('get_recordset_sql')
1136 ->willReturnOnConsecutiveCalls(
1137 new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1138 new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1140 $result = $c->get_progress_all(true, 3);
1142 $resultok = $resultok && ($ids == array_keys($result));
1144 foreach ($result as $userid => $data) {
1145 $resultok = $resultok && $data->firstname
== 'frog';
1146 $resultok = $resultok && $data->lastname
== $userid;
1147 $resultok = $resultok && $data->id
== $userid;
1148 $cms = $data->progress
;
1149 $resultok = $resultok && (array(13, 14) == array_keys($cms));
1150 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1151 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1153 $this->assertTrue($resultok);
1154 $this->assertCount(count($tracked), $result);
1158 * @covers ::inform_grade_changed
1160 public function test_inform_grade_changed() {
1161 $this->mock_setup();
1163 $mockbuilder = $this->getMockBuilder('completion_info');
1164 $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1165 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1167 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1168 $item = (object)array('itemnumber' => 3, 'gradepass' => 1, 'hidden' => 0);
1169 $grade = (object)array('userid' => 31337, 'finalgrade' => 0, 'rawgrade' => 0);
1171 // Not enabled (should do nothing).
1172 $c = $mockbuilder->getMock();
1173 $c->expects($this->once())
1174 ->method('is_enabled')
1176 ->will($this->returnValue(false));
1177 $c->inform_grade_changed($cm, $item, $grade, false);
1179 // Enabled but still no grade completion required, should still do nothing.
1180 $c = $mockbuilder->getMock();
1181 $c->expects($this->once())
1182 ->method('is_enabled')
1184 ->will($this->returnValue(true));
1185 $c->inform_grade_changed($cm, $item, $grade, false);
1187 // Enabled and completion required but item number is wrong, does nothing.
1188 $c = $mockbuilder->getMock();
1189 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1190 $c->expects($this->once())
1191 ->method('is_enabled')
1193 ->will($this->returnValue(true));
1194 $c->inform_grade_changed($cm, $item, $grade, false);
1196 // Enabled and completion required and item number right. It is supposed
1197 // to call update_state with the new potential state being obtained from
1198 // internal_get_grade_state.
1199 $c = $mockbuilder->getMock();
1200 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1201 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1202 $c->expects($this->once())
1203 ->method('is_enabled')
1205 ->will($this->returnValue(true));
1206 $c->expects($this->once())
1207 ->method('update_state')
1208 ->with($cm, COMPLETION_COMPLETE_PASS
, 31337)
1209 ->will($this->returnValue(true));
1210 $c->inform_grade_changed($cm, $item, $grade, false);
1212 // Same as above but marked deleted. It is supposed to call update_state
1213 // with new potential state being COMPLETION_INCOMPLETE.
1214 $c = $mockbuilder->getMock();
1215 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1216 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1217 $c->expects($this->once())
1218 ->method('is_enabled')
1220 ->will($this->returnValue(true));
1221 $c->expects($this->once())
1222 ->method('update_state')
1223 ->with($cm, COMPLETION_INCOMPLETE
, 31337)
1224 ->will($this->returnValue(true));
1225 $c->inform_grade_changed($cm, $item, $grade, true);
1229 * @covers ::internal_get_grade_state
1231 public function test_internal_get_grade_state() {
1232 $this->mock_setup();
1234 $item = new stdClass
;
1235 $grade = new stdClass
;
1237 $item->gradepass
= 4;
1239 $grade->rawgrade
= 4.0;
1240 $grade->finalgrade
= null;
1242 // Grade has pass mark and is not hidden, user passes.
1243 $this->assertEquals(
1244 COMPLETION_COMPLETE_PASS
,
1245 completion_info
::internal_get_grade_state($item, $grade));
1247 // Same but user fails.
1248 $grade->rawgrade
= 3.9;
1249 $this->assertEquals(
1250 COMPLETION_COMPLETE_FAIL
,
1251 completion_info
::internal_get_grade_state($item, $grade));
1253 // User fails on raw grade but passes on final.
1254 $grade->finalgrade
= 4.0;
1255 $this->assertEquals(
1256 COMPLETION_COMPLETE_PASS
,
1257 completion_info
::internal_get_grade_state($item, $grade));
1261 $this->assertEquals(
1262 COMPLETION_COMPLETE
,
1263 completion_info
::internal_get_grade_state($item, $grade));
1265 // Item isn't hidden but has no pass mark.
1267 $item->gradepass
= 0;
1268 $this->assertEquals(
1269 COMPLETION_COMPLETE
,
1270 completion_info
::internal_get_grade_state($item, $grade));
1274 * @test ::get_activities
1276 public function test_get_activities() {
1278 $this->resetAfterTest();
1280 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1281 $CFG->enablecompletion
= true;
1283 // Create a course with mixed auto completion data.
1284 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1285 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1286 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL
);
1287 $completionnone = array('completion' => COMPLETION_TRACKING_NONE
);
1288 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionauto);
1289 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id
), $completionauto);
1290 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id
), $completionmanual);
1292 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionnone);
1293 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id
), $completionnone);
1294 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id
), $completionnone);
1296 // Create data in another course to make sure it's not considered.
1297 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1298 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id
), $completionauto);
1299 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id
), $completionmanual);
1300 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id
), $completionnone);
1302 $c = new completion_info($course);
1303 $activities = $c->get_activities();
1304 $this->assertCount(3, $activities);
1305 $this->assertTrue(isset($activities[$forum->cmid
]));
1306 $this->assertSame($forum->name
, $activities[$forum->cmid
]->name
);
1307 $this->assertTrue(isset($activities[$page->cmid
]));
1308 $this->assertSame($page->name
, $activities[$page->cmid
]->name
);
1309 $this->assertTrue(isset($activities[$data->cmid
]));
1310 $this->assertSame($data->name
, $activities[$data->cmid
]->name
);
1312 $this->assertFalse(isset($activities[$forum2->cmid
]));
1313 $this->assertFalse(isset($activities[$page2->cmid
]));
1314 $this->assertFalse(isset($activities[$data2->cmid
]));
1318 * @test ::has_activities
1320 public function test_has_activities() {
1322 $this->resetAfterTest();
1324 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1325 $CFG->enablecompletion
= true;
1327 // Create a course with mixed auto completion data.
1328 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1329 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1330 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1331 $completionnone = array('completion' => COMPLETION_TRACKING_NONE
);
1332 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionauto);
1333 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id
), $completionnone);
1335 $c1 = new completion_info($course);
1336 $c2 = new completion_info($course2);
1338 $this->assertTrue($c1->has_activities());
1339 $this->assertFalse($c2->has_activities());
1343 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1345 * @covers ::delete_course_completion_data
1346 * @covers ::delete_all_completion_data
1348 public function test_course_delete_prerequisite() {
1351 $this->setup_data();
1353 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1355 $criteriadata = (object) [
1356 'id' => $this->course
->id
,
1357 'criteria_course' => [$courseprerequisite->id
],
1360 /** @var completion_criteria_course $criteria */
1361 $criteria = completion_criteria
::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
]);
1362 $criteria->update_config($criteriadata);
1365 $this->assertTrue($DB->record_exists('course_completion_criteria', [
1366 'course' => $this->course
->id
,
1367 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
,
1368 'courseinstance' => $courseprerequisite->id
,
1371 // Deleting the prerequisite course should remove the completion criteria.
1372 delete_course($courseprerequisite, false);
1374 $this->assertFalse($DB->record_exists('course_completion_criteria', [
1375 'course' => $this->course
->id
,
1376 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
,
1377 'courseinstance' => $courseprerequisite->id
,
1382 * Test course module completion update event.
1384 * @covers \core\event\course_module_completion_updated
1386 public function test_course_module_completion_updated_event() {
1389 $this->setup_data();
1391 $this->setAdminUser();
1393 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1394 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
1396 $c = new completion_info($this->course
);
1397 $activities = $c->get_activities();
1398 $this->assertEquals(1, count($activities));
1399 $this->assertTrue(isset($activities[$forum->cmid
]));
1400 $this->assertEquals($activities[$forum->cmid
]->name
, $forum->name
);
1402 $current = $c->get_data($activities[$forum->cmid
], false, $this->user
->id
);
1403 $current->completionstate
= COMPLETION_COMPLETE
;
1404 $current->timemodified
= time();
1405 $sink = $this->redirectEvents();
1406 $c->internal_set_data($activities[$forum->cmid
], $current);
1407 $events = $sink->get_events();
1408 $event = reset($events);
1409 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1410 $this->assertEquals($forum->cmid
,
1411 $event->get_record_snapshot('course_modules_completion', $event->objectid
)->coursemoduleid
);
1412 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid
));
1413 $this->assertEquals(context_module
::instance($forum->cmid
), $event->get_context());
1414 $this->assertEquals($USER->id
, $event->userid
);
1415 $this->assertEquals($this->user
->id
, $event->relateduserid
);
1416 $this->assertInstanceOf('moodle_url', $event->get_url());
1417 $this->assertEventLegacyData($current, $event);
1421 * Test course completed event.
1423 * @covers \core\event\course_completed
1425 public function test_course_completed_event() {
1428 $this->setup_data();
1429 $this->setAdminUser();
1431 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1432 $ccompletion = new completion_completion(array('course' => $this->course
->id
, 'userid' => $this->user
->id
));
1434 // Mark course as complete and get triggered event.
1435 $sink = $this->redirectEvents();
1436 $ccompletion->mark_complete();
1437 $events = $sink->get_events();
1438 $event = reset($events);
1440 $this->assertInstanceOf('\core\event\course_completed', $event);
1441 $this->assertEquals($this->course
->id
, $event->get_record_snapshot('course_completions', $event->objectid
)->course
);
1442 $this->assertEquals($this->course
->id
, $event->courseid
);
1443 $this->assertEquals($USER->id
, $event->userid
);
1444 $this->assertEquals($this->user
->id
, $event->relateduserid
);
1445 $this->assertEquals(context_course
::instance($this->course
->id
), $event->get_context());
1446 $this->assertInstanceOf('moodle_url', $event->get_url());
1447 $data = $ccompletion->get_record_data();
1448 $this->assertEventLegacyData($data, $event);
1452 * Test course completed message.
1454 * @covers \core\event\course_completed
1456 public function test_course_completed_message() {
1457 $this->setup_data();
1458 $this->setAdminUser();
1460 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1461 $ccompletion = new completion_completion(array('course' => $this->course
->id
, 'userid' => $this->user
->id
));
1463 // Mark course as complete and get the message.
1464 $sink = $this->redirectMessages();
1465 $ccompletion->mark_complete();
1466 $messages = $sink->get_messages();
1469 $this->assertCount(1, $messages);
1470 $message = array_pop($messages);
1472 $this->assertEquals(core_user
::get_noreply_user()->id
, $message->useridfrom
);
1473 $this->assertEquals($this->user
->id
, $message->useridto
);
1474 $this->assertEquals('coursecompleted', $message->eventtype
);
1475 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject
);
1476 $this->assertStringContainsString($this->course
->fullname
, $message->fullmessage
);
1480 * Test course completed event.
1482 * @covers \core\event\course_completion_updated
1484 public function test_course_completion_updated_event() {
1485 $this->setup_data();
1486 $coursecontext = context_course
::instance($this->course
->id
);
1487 $coursecompletionevent = \core\event\course_completion_updated
::create(
1489 'courseid' => $this->course
->id
,
1490 'context' => $coursecontext
1494 // Mark course as complete and get triggered event.
1495 $sink = $this->redirectEvents();
1496 $coursecompletionevent->trigger();
1497 $events = $sink->get_events();
1498 $event = array_pop($events);
1501 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1502 $this->assertEquals($this->course
->id
, $event->courseid
);
1503 $this->assertEquals($coursecontext, $event->get_context());
1504 $this->assertInstanceOf('moodle_url', $event->get_url());
1505 $expectedlegacylog = array($this->course
->id
, 'course', 'completion updated', 'completion.php?id='.$this->course
->id
);
1506 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1510 * @covers \completion_can_view_data
1512 public function test_completion_can_view_data() {
1513 $this->setup_data();
1515 $student = $this->getDataGenerator()->create_user();
1516 $this->getDataGenerator()->enrol_user($student->id
, $this->course
->id
);
1518 $this->setUser($student);
1519 $this->assertTrue(completion_can_view_data($student->id
, $this->course
->id
));
1520 $this->assertFalse(completion_can_view_data($this->user
->id
, $this->course
->id
));
1524 * Data provider for test_get_grade_completion().
1528 public function get_grade_completion_provider() {
1530 'Grade not required' => [false, false, null, null, null],
1531 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE
],
1532 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE
],
1533 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS
],
1534 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL
],
1539 * Test for \completion_info::get_grade_completion().
1541 * @dataProvider get_grade_completion_provider
1542 * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1543 * @param bool $hasgrade Whether to set grade for the user in this activity.
1544 * @param int|null $passinggrade Passing grade to set for the test activity.
1545 * @param string|null $expectedexception Expected exception.
1546 * @param int|null $expectedresult The expected completion status.
1547 * @covers ::get_grade_completion
1549 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?
int $passinggrade,
1550 ?
string $expectedexception, ?
int $expectedresult) {
1551 $this->setup_data();
1553 /** @var \mod_assign_generator $assigngenerator */
1554 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1555 $assign = $assigngenerator->create_instance([
1556 'course' => $this->course
->id
,
1557 'completion' => COMPLETION_ENABLED
,
1558 'completionusegrade' => $completionusegrade,
1559 'gradepass' => $passinggrade,
1562 $cm = cm_info
::create(get_coursemodule_from_instance('assign', $assign->id
));
1563 if ($completionusegrade && $hasgrade) {
1564 $assigninstance = new assign($cm->context
, $cm, $this->course
);
1565 $grade = $assigninstance->get_user_grade($this->user
->id
, true);
1567 $assigninstance->update_grade($grade);
1570 $completioninfo = new completion_info($this->course
);
1571 if ($expectedexception) {
1572 $this->expectException($expectedexception);
1574 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user
->id
);
1575 $this->assertEquals($expectedresult, $gradecompletion);
1579 * Test the return value for cases when the activity module does not have associated grade_item.
1581 * @covers ::get_grade_completion
1583 public function test_get_grade_completion_without_grade_item() {
1586 $this->setup_data();
1588 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1589 'course' => $this->course
->id
,
1590 'completion' => COMPLETION_ENABLED
,
1591 'completionusegrade' => true,
1595 $cm = cm_info
::create(get_coursemodule_from_instance('assign', $assign->id
));
1597 $DB->delete_records('grade_items', [
1598 'courseid' => $this->course
->id
,
1599 'itemtype' => 'mod',
1600 'itemmodule' => 'assign',
1601 'iteminstance' => $assign->id
,
1604 // Without the grade_item, the activity is considered incomplete.
1605 $completioninfo = new completion_info($this->course
);
1606 $this->assertEquals(COMPLETION_INCOMPLETE
, $completioninfo->get_grade_completion($cm, $this->user
->id
));
1608 // Once the activity is graded, the grade_item is automatically created.
1609 $assigninstance = new assign($cm->context
, $cm, $this->course
);
1610 $grade = $assigninstance->get_user_grade($this->user
->id
, true);
1612 $assigninstance->update_grade($grade);
1614 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1615 $this->assertEquals(COMPLETION_COMPLETE
, $completioninfo->get_grade_completion($cm, $this->user
->id
));
1619 * Test for aggregate_completions().
1621 * @covers \aggregate_completions
1623 public function test_aggregate_completions() {
1625 $this->resetAfterTest(true);
1628 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1630 for ($i = 0; $i < 4; $i++
) {
1631 $students[] = $this->getDataGenerator()->create_user();
1634 $teacher = $this->getDataGenerator()->create_user();
1635 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1636 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1638 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1639 foreach ($students as $student) {
1640 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1643 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id
),
1644 array('completion' => 1));
1645 $cmdata = get_coursemodule_from_id('data', $data->cmid
);
1647 // Add activity completion criteria.
1648 $criteriadata = new stdClass();
1649 $criteriadata->id
= $course->id
;
1650 $criteriadata->criteria_activity
= array();
1652 $criteriadata->criteria_activity
[$cmdata->id
] = 1;
1653 $class = 'completion_criteria_activity';
1654 $criterion = new $class();
1655 $criterion->update_config($criteriadata);
1657 $this->setUser($teacher);
1659 // Mark activity complete for both students.
1660 $cm = get_coursemodule_from_instance('data', $data->id
);
1661 $completioncriteria = $DB->get_record('course_completion_criteria', []);
1662 foreach ($students as $student) {
1663 $cmcompletionrecords[] = (object)[
1664 'coursemoduleid' => $cm->id
,
1665 'userid' => $student->id
,
1666 'completionstate' => 1,
1668 'overrideby' => null,
1669 'timemodified' => 0,
1672 $usercompletions[] = (object)[
1673 'criteriaid' => $completioncriteria->id
,
1674 'userid' => $student->id
,
1675 'timecompleted' => $time,
1679 'course' => $course->id
,
1680 'userid' => $student->id
1682 $ccompletion = new completion_completion($cc);
1683 $completion[] = $ccompletion->mark_inprogress($time);
1685 $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1686 $DB->insert_records('course_completion_crit_compl', $usercompletions);
1688 // MDL-33320: for instant completions we need aggregate to work in a single run.
1689 $DB->set_field('course_completions', 'reaggregate', $time - 2);
1691 foreach ($students as $student) {
1692 $result = $DB->get_record('course_completions', ['userid' => $student->id
, 'reaggregate' => 0]);
1693 $this->assertFalse($result);
1696 aggregate_completions($completion[0]);
1698 $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id
, 'reaggregate' => 0]);
1699 $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id
, 'reaggregate' => 0]);
1700 $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id
, 'reaggregate' => 0]);
1702 $this->assertIsObject($result1);
1703 $this->assertFalse($result2);
1704 $this->assertFalse($result3);
1706 aggregate_completions(0);
1708 foreach ($students as $student) {
1709 $result = $DB->get_record('course_completions', ['userid' => $student->id
, 'reaggregate' => 0]);
1710 $this->assertIsObject($result);
1715 * Test for completion_completion::_save().
1717 * @covers \completion_completion::_save
1719 public function test_save() {
1721 $this->resetAfterTest(true);
1723 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1725 $student = $this->getDataGenerator()->create_user();
1726 $teacher = $this->getDataGenerator()->create_user();
1727 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1728 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1730 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1731 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1733 $this->setUser($teacher);
1736 'course' => $course->id
,
1737 'userid' => $student->id
1739 $ccompletion = new completion_completion($cc);
1741 $completions = $DB->get_records('course_completions');
1742 $this->assertEmpty($completions);
1744 // We're testing a private method, so we need to setup reflector magic.
1745 $method = new ReflectionMethod($ccompletion, '_save');
1746 $method->setAccessible(true); // Allow accessing of private method.
1747 $completionid = $method->invoke($ccompletion);
1748 $completions = $DB->get_records('course_completions');
1749 $this->assertEquals(count($completions), 1);
1750 $this->assertEquals(reset($completions)->id
, $completionid);
1752 $ccompletion->id
= 0;
1753 $method = new ReflectionMethod($ccompletion, '_save');
1754 $method->setAccessible(true); // Allow accessing of private method.
1755 $completionid = $method->invoke($ccompletion);
1756 $this->assertDebuggingCalled('Can not update data object, no id!');
1757 $this->assertNull($completionid);
1761 * Test for completion_completion::mark_enrolled().
1763 * @covers \completion_completion::mark_enrolled
1765 public function test_mark_enrolled() {
1767 $this->resetAfterTest(true);
1769 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1771 $student = $this->getDataGenerator()->create_user();
1772 $teacher = $this->getDataGenerator()->create_user();
1773 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1774 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1776 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1777 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1779 $this->setUser($teacher);
1782 'course' => $course->id
,
1783 'userid' => $student->id
1785 $ccompletion = new completion_completion($cc);
1787 $completions = $DB->get_records('course_completions');
1788 $this->assertEmpty($completions);
1790 $completionid = $ccompletion->mark_enrolled();
1791 $completions = $DB->get_records('course_completions');
1792 $this->assertEquals(count($completions), 1);
1793 $this->assertEquals(reset($completions)->id
, $completionid);
1795 $ccompletion->id
= 0;
1796 $completionid = $ccompletion->mark_enrolled();
1797 $this->assertDebuggingCalled('Can not update data object, no id!');
1798 $this->assertNull($completionid);
1799 $completions = $DB->get_records('course_completions');
1800 $this->assertEquals(1, count($completions));
1804 * Test for completion_completion::mark_inprogress().
1806 * @covers \completion_completion::mark_inprogress
1808 public function test_mark_inprogress() {
1810 $this->resetAfterTest(true);
1812 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1814 $student = $this->getDataGenerator()->create_user();
1815 $teacher = $this->getDataGenerator()->create_user();
1816 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1817 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1819 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1820 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1822 $this->setUser($teacher);
1825 'course' => $course->id
,
1826 'userid' => $student->id
1828 $ccompletion = new completion_completion($cc);
1830 $completions = $DB->get_records('course_completions');
1831 $this->assertEmpty($completions);
1833 $completionid = $ccompletion->mark_inprogress();
1834 $completions = $DB->get_records('course_completions');
1835 $this->assertEquals(1, count($completions));
1836 $this->assertEquals(reset($completions)->id
, $completionid);
1838 $ccompletion->id
= 0;
1839 $completionid = $ccompletion->mark_inprogress();
1840 $this->assertDebuggingCalled('Can not update data object, no id!');
1841 $this->assertNull($completionid);
1842 $completions = $DB->get_records('course_completions');
1843 $this->assertEquals(1, count($completions));
1847 * Test for completion_completion::mark_complete().
1849 * @covers \completion_completion::mark_complete
1851 public function test_mark_complete() {
1853 $this->resetAfterTest(true);
1855 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1857 $student = $this->getDataGenerator()->create_user();
1858 $teacher = $this->getDataGenerator()->create_user();
1859 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1860 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1862 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1863 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1865 $this->setUser($teacher);
1868 'course' => $course->id
,
1869 'userid' => $student->id
1871 $ccompletion = new completion_completion($cc);
1873 $completions = $DB->get_records('course_completions');
1874 $this->assertEmpty($completions);
1876 $completionid = $ccompletion->mark_complete();
1877 $completions = $DB->get_records('course_completions');
1878 $this->assertEquals(1, count($completions));
1879 $this->assertEquals(reset($completions)->id
, $completionid);
1881 $ccompletion->id
= 0;
1882 $completionid = $ccompletion->mark_complete();
1883 $this->assertNull($completionid);
1884 $completions = $DB->get_records('course_completions');
1885 $this->assertEquals(1, count($completions));
1889 * Test for completion_criteria_completion::mark_complete().
1891 * @covers \completion_criteria_completion::mark_complete
1893 public function test_criteria_mark_complete() {
1895 $this->resetAfterTest(true);
1897 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1899 $student = $this->getDataGenerator()->create_user();
1900 $teacher = $this->getDataGenerator()->create_user();
1901 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1902 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1904 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1905 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1907 $this->setUser($teacher);
1910 'course' => $course->id
,
1912 'userid' => $student->id
,
1913 'timecompleted' => time()
1915 $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY
);
1917 $completions = $DB->get_records('course_completions');
1918 $this->assertEmpty($completions);
1920 $completionid = $completion->mark_complete($record['timecompleted']);
1921 $completions = $DB->get_records('course_completions');
1922 $this->assertEquals(1, count($completions));
1923 $this->assertEquals(reset($completions)->id
, $completionid);
1927 class core_completionlib_fake_recordset
implements Iterator
{
1929 protected $values, $index;
1931 public function __construct($values) {
1932 $this->values
= $values;
1936 public function current() {
1937 return $this->values
[$this->index
];
1940 public function key() {
1941 return $this->values
[$this->index
];
1944 public function next() {
1948 public function rewind() {
1952 public function valid() {
1953 return count($this->values
) > $this->index
;
1956 public function close() {
1957 $this->closed
= true;
1960 public function was_closed() {
1961 return $this->closed
;