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/>.
17 defined('MOODLE_INTERNAL') ||
die();
20 require_once($CFG->libdir
.'/completionlib.php');
25 * @package core_completion
27 * @copyright 2008 Sam Marshall
28 * @copyright 2013 Frédéric Massart
29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 * @coversDefaultClass \completion_info
32 class completionlib_test
extends advanced_testcase
{
38 protected function mock_setup() {
39 global $DB, $CFG, $USER;
41 $this->resetAfterTest();
43 $DB = $this->createMock(get_class($DB));
44 $CFG->enablecompletion
= COMPLETION_ENABLED
;
45 $USER = (object)array('id' => 314159);
49 * Create course with user and activities.
51 protected function setup_data() {
54 $this->resetAfterTest();
56 // Enable completion before creating modules, otherwise the completion data is not written in DB.
57 $CFG->enablecompletion
= true;
59 // Create a course with activities.
60 $this->course
= $this->getDataGenerator()->create_course(array('enablecompletion' => true));
61 $this->user
= $this->getDataGenerator()->create_user();
62 $this->getDataGenerator()->enrol_user($this->user
->id
, $this->course
->id
);
64 $this->module1
= $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
));
65 $this->module2
= $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
));
69 * Asserts that two variables are equal.
71 * @param mixed $expected
72 * @param mixed $actual
73 * @param string $message
75 * @param integer $maxDepth
76 * @param boolean $canonicalize
77 * @param boolean $ignoreCase
79 public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
80 bool $canonicalize = false, bool $ignoreCase = false): void
{
81 // Nasty cheating hack: prevent random failures on timemodified field.
82 if (is_array($actual) && (is_object($expected) ||
is_array($expected))) {
83 $actual = (object) $actual;
84 $expected = (object) $expected;
86 if (is_object($expected) and is_object($actual)) {
87 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
88 if ($expected->timemodified +
1 == $actual->timemodified
) {
89 $expected = clone($expected);
90 $expected->timemodified
= $actual->timemodified
;
94 parent
::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
98 * @covers ::is_enabled_for_site
99 * @covers ::is_enabled
101 public function test_is_enabled() {
106 $CFG->enablecompletion
= COMPLETION_DISABLED
;
107 $this->assertEquals(COMPLETION_DISABLED
, completion_info
::is_enabled_for_site());
108 $CFG->enablecompletion
= COMPLETION_ENABLED
;
109 $this->assertEquals(COMPLETION_ENABLED
, completion_info
::is_enabled_for_site());
112 $course = (object)array('id' => 13);
113 $c = new completion_info($course);
114 $course->enablecompletion
= COMPLETION_DISABLED
;
115 $this->assertEquals(COMPLETION_DISABLED
, $c->is_enabled());
116 $course->enablecompletion
= COMPLETION_ENABLED
;
117 $this->assertEquals(COMPLETION_ENABLED
, $c->is_enabled());
118 $CFG->enablecompletion
= COMPLETION_DISABLED
;
119 $this->assertEquals(COMPLETION_DISABLED
, $c->is_enabled());
122 $cm = new stdClass();
123 $cm->completion
= COMPLETION_TRACKING_MANUAL
;
124 $this->assertEquals(COMPLETION_DISABLED
, $c->is_enabled($cm));
125 $CFG->enablecompletion
= COMPLETION_ENABLED
;
126 $course->enablecompletion
= COMPLETION_DISABLED
;
127 $this->assertEquals(COMPLETION_DISABLED
, $c->is_enabled($cm));
128 $course->enablecompletion
= COMPLETION_ENABLED
;
129 $this->assertEquals(COMPLETION_TRACKING_MANUAL
, $c->is_enabled($cm));
130 $cm->completion
= COMPLETION_TRACKING_NONE
;
131 $this->assertEquals(COMPLETION_TRACKING_NONE
, $c->is_enabled($cm));
132 $cm->completion
= COMPLETION_TRACKING_AUTOMATIC
;
133 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC
, $c->is_enabled($cm));
137 * @covers ::update_state
139 public function test_update_state() {
142 $mockbuilder = $this->getMockBuilder('completion_info');
143 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
144 'user_can_override_completion'));
145 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
146 $cm = (object)array('id' => 13, 'course' => 42);
148 // Not enabled, should do nothing.
149 $c = $mockbuilder->getMock();
150 $c->expects($this->once())
151 ->method('is_enabled')
153 ->will($this->returnValue(false));
154 $c->update_state($cm);
156 // Enabled, but current state is same as possible result, do nothing.
157 $cm->completion
= COMPLETION_TRACKING_AUTOMATIC
;
158 $c = $mockbuilder->getMock();
159 $current = (object)array('completionstate' => COMPLETION_COMPLETE
, 'overrideby' => null);
160 $c->expects($this->once())
161 ->method('is_enabled')
163 ->will($this->returnValue(true));
164 $c->expects($this->once())
166 ->will($this->returnValue($current));
167 $c->update_state($cm, COMPLETION_COMPLETE
);
169 // Enabled, but current state is a specific one and new state is just
170 // complete, so do nothing.
171 $c = $mockbuilder->getMock();
172 $current->completionstate
= COMPLETION_COMPLETE_PASS
;
173 $c->expects($this->once())
174 ->method('is_enabled')
176 ->will($this->returnValue(true));
177 $c->expects($this->once())
179 ->will($this->returnValue($current));
180 $c->update_state($cm, COMPLETION_COMPLETE
);
182 // Manual, change state (no change).
183 $c = $mockbuilder->getMock();
184 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL
);
185 $current->completionstate
= COMPLETION_COMPLETE
;
186 $c->expects($this->once())
187 ->method('is_enabled')
189 ->will($this->returnValue(true));
190 $c->expects($this->once())
192 ->will($this->returnValue($current));
193 $c->update_state($cm, COMPLETION_COMPLETE
);
195 // Manual, change state (change).
196 $c = $mockbuilder->getMock();
197 $c->expects($this->once())
198 ->method('is_enabled')
200 ->will($this->returnValue(true));
201 $c->expects($this->once())
203 ->will($this->returnValue($current));
204 $changed = clone($current);
205 $changed->timemodified
= time();
206 $changed->completionstate
= COMPLETION_INCOMPLETE
;
207 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
208 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
209 $c->expects($this->once())
210 ->method('internal_set_data')
211 ->with($cm, $comparewith);
212 $c->update_state($cm, COMPLETION_INCOMPLETE
);
214 // Auto, change state.
215 $c = $mockbuilder->getMock();
216 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC
);
217 $current = (object)array('completionstate' => COMPLETION_COMPLETE
, 'overrideby' => null);
218 $c->expects($this->once())
219 ->method('is_enabled')
221 ->will($this->returnValue(true));
222 $c->expects($this->once())
224 ->will($this->returnValue($current));
225 $c->expects($this->once())
226 ->method('internal_get_state')
227 ->will($this->returnValue(COMPLETION_COMPLETE_PASS
));
228 $changed = clone($current);
229 $changed->timemodified
= time();
230 $changed->completionstate
= COMPLETION_COMPLETE_PASS
;
231 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
232 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
233 $c->expects($this->once())
234 ->method('internal_set_data')
235 ->with($cm, $comparewith);
236 $c->update_state($cm, COMPLETION_COMPLETE_PASS
);
238 // Manual tracking, change state by overriding it manually.
239 $c = $mockbuilder->getMock();
240 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL
);
241 $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE
, 'overrideby' => null);
242 $current2 = (object)array('completionstate' => COMPLETION_COMPLETE
, 'overrideby' => null);
243 $c->expects($this->exactly(2))
244 ->method('is_enabled')
246 ->will($this->returnValue(true));
247 $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
248 ->method('user_can_override_completion')
249 ->will($this->returnValue(true));
250 $c->expects($this->exactly(2))
252 ->with($cm, false, 100)
253 ->willReturnOnConsecutiveCalls($current1, $current2);
254 $changed1 = clone($current1);
255 $changed1->timemodified
= time();
256 $changed1->completionstate
= COMPLETION_COMPLETE
;
257 $changed1->overrideby
= 314159;
258 $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
259 $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
260 $changed2 = clone($current2);
261 $changed2->timemodified
= time();
262 $changed2->overrideby
= null;
263 $changed2->completionstate
= COMPLETION_INCOMPLETE
;
264 $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
265 $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
266 $c->expects($this->exactly(2))
267 ->method('internal_set_data')
269 array($cm, $comparewith1),
270 array($cm, $comparewith2)
272 $c->update_state($cm, COMPLETION_COMPLETE
, 100, true);
273 // And confirm that the status can be changed back to incomplete without an override.
274 $c->update_state($cm, COMPLETION_INCOMPLETE
, 100);
276 // Auto, change state via override, incomplete to complete.
277 $c = $mockbuilder->getMock();
278 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC
);
279 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE
, 'overrideby' => null);
280 $c->expects($this->once())
281 ->method('is_enabled')
283 ->will($this->returnValue(true));
284 $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
285 ->method('user_can_override_completion')
286 ->will($this->returnValue(true));
287 $c->expects($this->once())
289 ->with($cm, false, 100)
290 ->will($this->returnValue($current));
291 $changed = clone($current);
292 $changed->timemodified
= time();
293 $changed->completionstate
= COMPLETION_COMPLETE
;
294 $changed->overrideby
= 314159;
295 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
296 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
297 $c->expects($this->once())
298 ->method('internal_set_data')
299 ->with($cm, $comparewith);
300 $c->update_state($cm, COMPLETION_COMPLETE
, 100, true);
302 // Now confirm the status can be changed back from complete to incomplete using an override.
303 $c = $mockbuilder->getMock();
304 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC
);
305 $current = (object)array('completionstate' => COMPLETION_COMPLETE
, 'overrideby' => 2);
306 $c->expects($this->once())
307 ->method('is_enabled')
309 ->will($this->returnValue(true));
310 $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
311 ->method('user_can_override_completion')
312 ->will($this->returnValue(true));
313 $c->expects($this->once())
315 ->with($cm, false, 100)
316 ->will($this->returnValue($current));
317 $changed = clone($current);
318 $changed->timemodified
= time();
319 $changed->completionstate
= COMPLETION_INCOMPLETE
;
320 $changed->overrideby
= 314159;
321 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
322 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
323 $c->expects($this->once())
324 ->method('internal_set_data')
325 ->with($cm, $comparewith);
326 $c->update_state($cm, COMPLETION_INCOMPLETE
, 100, true);
330 * Data provider for test_internal_get_state().
334 public function internal_get_state_provider() {
336 'View required, but not viewed yet' => [
337 COMPLETION_VIEW_REQUIRED
, 1, '', COMPLETION_INCOMPLETE
339 'View not required and not viewed yet' => [
340 COMPLETION_VIEW_NOT_REQUIRED
, 1, '', COMPLETION_INCOMPLETE
342 'View not required, grade required but no grade yet, $cm->modname not set' => [
343 COMPLETION_VIEW_NOT_REQUIRED
, 1, 'modname', COMPLETION_INCOMPLETE
345 'View not required, grade required but no grade yet, $cm->course not set' => [
346 COMPLETION_VIEW_NOT_REQUIRED
, 1, 'course', COMPLETION_INCOMPLETE
348 'View not required, grade not required' => [
349 COMPLETION_VIEW_NOT_REQUIRED
, 0, '', COMPLETION_COMPLETE
355 * Test for completion_info::get_state().
357 * @dataProvider internal_get_state_provider
358 * @param int $completionview
359 * @param int $completionusegrade
360 * @param string $unsetfield
361 * @param int $expectedstate
362 * @covers ::internal_get_state
364 public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
367 /** @var \mod_assign_generator $assigngenerator */
368 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
369 $assign = $assigngenerator->create_instance([
370 'course' => $this->course
->id
,
371 'completion' => COMPLETION_ENABLED
,
372 'completionview' => $completionview,
373 'completionusegrade' => $completionusegrade,
376 $userid = $this->user
->id
;
377 $this->setUser($userid);
379 $cm = get_coursemodule_from_instance('assign', $assign->id
);
381 unset($cm->$unsetfield);
383 // If view is required, but they haven't viewed it yet.
384 $current = (object)['viewed' => COMPLETION_NOT_VIEWED
];
386 $completioninfo = new completion_info($this->course
);
387 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
391 * Provider for the test_internal_get_state_with_grade_criteria.
395 public function internal_get_state_with_grade_criteria_provider() {
397 "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
399 'completionusegrade' => 1,
400 'completionpassgrade' => 1,
404 COMPLETION_COMPLETE_PASS
406 "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
408 'completionusegrade' => 1,
409 'completionpassgrade' => 1,
413 COMPLETION_COMPLETE_FAIL
415 "Passing grade not enabled with passing grade set." => [
417 'completionusegrade' => 1,
421 COMPLETION_COMPLETE_PASS
423 "Passing grade not enabled with passing grade not set." => [
425 'completionusegrade' => 1,
430 "Passing grade not enabled with passing grade not set. No submission made." => [
432 'completionusegrade' => 1,
435 COMPLETION_INCOMPLETE
441 * Tests that the right completion state is being set based on the grade criteria.
443 * @dataProvider internal_get_state_with_grade_criteria_provider
444 * @param array $completioncriteria The completion criteria to use
445 * @param int|null $studentgrade Grade to assign to student
446 * @param int $expectedstate Expected completion state
447 * @covers ::internal_get_state
449 public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?
int $studentgrade, int $expectedstate) {
452 /** @var \mod_assign_generator $assigngenerator */
453 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
454 $assign = $assigngenerator->create_instance([
455 'course' => $this->course
->id
,
456 'completion' => COMPLETION_ENABLED
,
457 ] +
$completioncriteria);
459 $userid = $this->user
->id
;
461 $cm = get_coursemodule_from_instance('assign', $assign->id
);
462 $usercm = cm_info
::create($cm, $userid);
464 // Create a teacher account.
465 $teacher = $this->getDataGenerator()->create_user();
466 $this->getDataGenerator()->enrol_user($teacher->id
, $this->course
->id
, 'editingteacher');
467 // Log in as the teacher.
468 $this->setUser($teacher);
470 // Grade the student for this assignment.
471 $assign = new assign($usercm->context
, $cm, $cm->course
);
474 'sendstudentnotifications' => false,
475 'attemptnumber' => 1,
476 'grade' => $studentgrade,
478 $assign->save_grade($userid, $data);
481 // The target user already received a grade, so internal_get_state should be already complete.
482 $completioninfo = new completion_info($this->course
);
483 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
487 * Covers the case where internal_get_state() is being called for a user different from the logged in user.
489 * @covers ::internal_get_state
491 public function test_internal_get_state_with_different_user() {
494 /** @var \mod_assign_generator $assigngenerator */
495 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
496 $assign = $assigngenerator->create_instance([
497 'course' => $this->course
->id
,
498 'completion' => COMPLETION_ENABLED
,
499 'completionusegrade' => 1,
502 $userid = $this->user
->id
;
504 $cm = get_coursemodule_from_instance('assign', $assign->id
);
505 $usercm = cm_info
::create($cm, $userid);
507 // Create a teacher account.
508 $teacher = $this->getDataGenerator()->create_user();
509 $this->getDataGenerator()->enrol_user($teacher->id
, $this->course
->id
, 'editingteacher');
510 // Log in as the teacher.
511 $this->setUser($teacher);
513 // Grade the student for this assignment.
514 $assign = new assign($usercm->context
, $cm, $cm->course
);
516 'sendstudentnotifications' => false,
517 'attemptnumber' => 1,
520 $assign->save_grade($userid, $data);
522 // The target user already received a grade, so internal_get_state should be already complete.
523 $completioninfo = new completion_info($this->course
);
524 $this->assertEquals(COMPLETION_COMPLETE
, $completioninfo->internal_get_state($cm, $userid, null));
526 // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
527 $this->assertEquals(COMPLETION_INCOMPLETE
, $completioninfo->internal_get_state($cm, $teacher->id
, null));
531 * Test for internal_get_state() for an activity that supports custom completion.
533 * @covers ::internal_get_state
535 public function test_internal_get_state_with_custom_completion() {
539 'course' => $this->course
,
540 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
541 'completionsubmit' => COMPLETION_ENABLED
,
543 $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
544 $cminfo = cm_info
::create(get_coursemodule_from_instance('choice', $choice->id
));
546 $completioninfo = new completion_info($this->course
);
548 // Fetch completion for the user who hasn't made a choice yet.
549 $completion = $completioninfo->internal_get_state($cminfo, $this->user
->id
, COMPLETION_INCOMPLETE
);
550 $this->assertEquals(COMPLETION_INCOMPLETE
, $completion);
552 // Have the user make a choice.
553 $choicewithoptions = choice_get_choice($choice->id
);
554 $optionids = array_keys($choicewithoptions->option
);
555 choice_user_submit_response($optionids[0], $choice, $this->user
->id
, $this->course
, $cminfo);
556 $completion = $completioninfo->internal_get_state($cminfo, $this->user
->id
, COMPLETION_INCOMPLETE
);
557 $this->assertEquals(COMPLETION_COMPLETE
, $completion);
561 * @covers ::set_module_viewed
563 public function test_set_module_viewed() {
566 $mockbuilder = $this->getMockBuilder('completion_info');
567 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
568 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
569 $cm = (object)array('id' => 13, 'course' => 42);
571 // Not tracking completion, should do nothing.
572 $c = $mockbuilder->getMock();
573 $cm->completionview
= COMPLETION_VIEW_NOT_REQUIRED
;
574 $c->set_module_viewed($cm);
576 // Tracking completion but completion is disabled, should do nothing.
577 $c = $mockbuilder->getMock();
578 $cm->completionview
= COMPLETION_VIEW_REQUIRED
;
579 $c->expects($this->once())
580 ->method('is_enabled')
582 ->will($this->returnValue(false));
583 $c->set_module_viewed($cm);
585 // Now it's enabled, we expect it to get data. If data already has
586 // viewed, still do nothing.
587 $c = $mockbuilder->getMock();
588 $c->expects($this->once())
589 ->method('is_enabled')
591 ->will($this->returnValue(true));
592 $c->expects($this->once())
595 ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED
)));
596 $c->set_module_viewed($cm);
598 // OK finally one that hasn't been viewed, now it should set it viewed
600 $c = $mockbuilder->getMock();
601 $c->expects($this->once())
602 ->method('is_enabled')
604 ->will($this->returnValue(true));
605 $c->expects($this->once())
607 ->with($cm, false, 1337)
608 ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED
)));
609 $c->expects($this->once())
610 ->method('internal_set_data')
611 ->with($cm, (object)array('viewed' => COMPLETION_VIEWED
));
612 $c->expects($this->once())
613 ->method('update_state')
614 ->with($cm, COMPLETION_COMPLETE
, 1337);
615 $c->set_module_viewed($cm, 1337);
619 * @covers ::count_user_data
621 public function test_count_user_data() {
625 $course = (object)array('id' => 13);
626 $cm = (object)array('id' => 42);
628 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
629 $DB->expects($this->once())
630 ->method('get_field_sql')
631 ->will($this->returnValue(666));
633 $c = new completion_info($course);
634 $this->assertEquals(666, $c->count_user_data($cm));
638 * @covers ::delete_all_state
640 public function test_delete_all_state() {
644 $course = (object)array('id' => 13);
645 $cm = (object)array('id' => 42, 'course' => 13);
646 $c = new completion_info($course);
648 // Check it works ok without data in session.
649 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
650 $DB->expects($this->once())
651 ->method('delete_records')
652 ->with('course_modules_completion', array('coursemoduleid' => 42))
653 ->will($this->returnValue(true));
654 $c->delete_all_state($cm);
658 * @covers ::reset_all_state
660 public function test_reset_all_state() {
664 $mockbuilder = $this->getMockBuilder('completion_info');
665 $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
666 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
667 $c = $mockbuilder->getMock();
669 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC
);
671 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
672 $DB->expects($this->once())
673 ->method('get_recordset')
674 ->will($this->returnValue(
675 new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
676 (object)array('id' => 2, 'userid' => 101)))));
678 $c->expects($this->once())
679 ->method('delete_all_state')
682 $c->expects($this->once())
683 ->method('get_tracked_users')
684 ->will($this->returnValue(array(
685 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
686 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
688 $c->expects($this->exactly(3))
689 ->method('update_state')
691 array($cm, COMPLETION_UNKNOWN
, 100),
692 array($cm, COMPLETION_UNKNOWN
, 101),
693 array($cm, COMPLETION_UNKNOWN
, 201)
696 $c->reset_all_state($cm);
700 * Data provider for test_get_data().
704 public function get_data_provider() {
706 'No completion record' => [
707 false, true, false, COMPLETION_INCOMPLETE
710 false, true, true, COMPLETION_INCOMPLETE
713 false, true, true, COMPLETION_COMPLETE
715 'Whole course, complete' => [
716 true, true, true, COMPLETION_COMPLETE
718 'Get data for another user, result should be not cached' => [
719 false, false, true, COMPLETION_INCOMPLETE
721 'Get data for another user, including whole course, result should be not cached' => [
722 true, false, true, COMPLETION_INCOMPLETE
728 * Tests for completion_info::get_data().
730 * @dataProvider get_data_provider
731 * @param bool $wholecourse Whole course parameter for get_data().
732 * @param bool $sameuser Whether the user calling get_data() is the user itself.
733 * @param bool $hasrecord Whether to create a course_modules_completion record.
734 * @param int $completion The completion state expected.
737 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
743 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
744 $choice = $choicegenerator->create_instance([
745 'course' => $this->course
->id
,
746 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
747 'completionview' => true,
748 'completionsubmit' => true,
751 $cm = get_coursemodule_from_instance('choice', $choice->id
);
753 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
755 $cmcompletionrecord = (object)[
756 'coursemoduleid' => $cm->id
,
757 'userid' => $user->id
,
758 'completionstate' => $completion,
760 'overrideby' => null,
763 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
766 // Whether we expect for the returned completion data to be stored in the cache.
771 $this->setAdminUser();
773 $this->setUser($user);
776 // Mock other completion data.
777 $completioninfo = new completion_info($this->course
);
779 $result = $completioninfo->get_data($cm, $wholecourse, $user->id
);
781 // Course module ID of the returned completion data must match this activity's course module ID.
782 $this->assertEquals($cm->id
, $result->coursemoduleid
);
783 // User ID of the returned completion data must match the user's ID.
784 $this->assertEquals($user->id
, $result->userid
);
785 // The completion state of the returned completion data must match the expected completion state.
786 $this->assertEquals($completion, $result->completionstate
);
788 // If the user has no completion record, then the default record should be returned.
790 $this->assertEquals(0, $result->id
);
793 // Check that we are including relevant completion data for the module.
795 $this->assertTrue(property_exists($result, 'viewed'));
796 $this->assertTrue(property_exists($result, 'customcompletion'));
803 public function test_get_data_successive_calls(): void
{
807 $this->setUser($this->user
);
809 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
810 $choice = $choicegenerator->create_instance([
811 'course' => $this->course
->id
,
812 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
813 'completionview' => true,
814 'completionsubmit' => true,
817 $cm = get_coursemodule_from_instance('choice', $choice->id
);
819 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
820 $cmcompletionrecord = (object) [
821 'coursemoduleid' => $cm->id
,
822 'userid' => $this->user
->id
,
823 'completionstate' => COMPLETION_NOT_VIEWED
,
825 'overrideby' => null,
828 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
830 // Mock other completion data.
831 $completioninfo = new completion_info($this->course
);
833 $modinfo = get_fast_modinfo($this->course
);
835 foreach ($modinfo->cms
as $testcm) {
836 $result = $completioninfo->get_data($testcm, true);
837 $this->assertTrue(property_exists($result, 'id'));
838 $this->assertTrue(property_exists($result, 'coursemoduleid'));
839 $this->assertTrue(property_exists($result, 'userid'));
840 $this->assertTrue(property_exists($result, 'completionstate'));
841 $this->assertTrue(property_exists($result, 'viewed'));
842 $this->assertTrue(property_exists($result, 'overrideby'));
843 $this->assertTrue(property_exists($result, 'timemodified'));
844 $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
846 $this->assertEquals($testcm->id
, $result->coursemoduleid
);
847 $this->assertEquals($this->user
->id
, $result->userid
);
848 $this->assertEquals(0, $result->viewed
);
850 $results[$testcm->id
] = $result;
853 $result = $completioninfo->get_data($cm);
854 $this->assertTrue(property_exists($result, 'customcompletion'));
856 // The data should match when fetching modules individually.
857 (cache
::make('core', 'completion'))->purge();
858 foreach ($modinfo->cms
as $testcm) {
859 $result = $completioninfo->get_data($testcm, false);
860 $this->assertEquals($result, $results[$testcm->id
]);
865 * Tests for completion_info::get_other_cm_completion_data().
867 * @covers ::get_other_cm_completion_data
869 public function test_get_other_cm_completion_data() {
875 $this->setAdminUser();
877 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
878 $choice = $choicegenerator->create_instance([
879 'course' => $this->course
->id
,
880 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
881 'completionsubmit' => true,
884 $cmchoice = cm_info
::create(get_coursemodule_from_instance('choice', $choice->id
));
886 $choice2 = $choicegenerator->create_instance([
887 'course' => $this->course
->id
,
888 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
891 $cmchoice2 = cm_info
::create(get_coursemodule_from_instance('choice', $choice2->id
));
893 $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
894 $workshop = $workshopgenerator->create_instance([
895 'course' => $this->course
->id
,
896 'completion' => COMPLETION_TRACKING_AUTOMATIC
,
897 // Submission grade required.
898 'completiongradeitemnumber' => 0,
899 'completionpassgrade' => 1,
902 $cmworkshop = cm_info
::create(get_coursemodule_from_instance('workshop', $workshop->id
));
904 $completioninfo = new completion_info($this->course
);
906 $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
907 $method->setAccessible(true);
909 // Check that fetching data for a module with custom completion provides its info.
910 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id
);
912 $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
913 $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
914 $this->assertEquals(COMPLETION_INCOMPLETE
, $choicecompletiondata['customcompletion']['completionsubmit']);
916 // Mock a choice answer so user has completed the requirement.
918 'choiceid' => $cmchoice->instance
,
919 'userid' => $this->user
->id
921 $DB->insert_record('choice_answers', $choicemockinfo, false);
923 // Confirm fetching again reflects the completion.
924 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id
);
925 $this->assertEquals(COMPLETION_COMPLETE
, $choicecompletiondata['customcompletion']['completionsubmit']);
927 // Check that fetching data for a module with no custom completion still provides its grade completion status.
928 $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id
);
930 $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
931 $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
932 $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
933 $this->assertEquals(COMPLETION_INCOMPLETE
, $workshopcompletiondata['completiongrade']);
934 $this->assertEquals(COMPLETION_INCOMPLETE
, $workshopcompletiondata['passgrade']);
936 // Check that fetching data for a module with no completion conditions does not provide any data.
937 $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id
);
938 $this->assertEmpty($choice2completiondata);
942 * @covers ::internal_set_data
944 public function test_internal_set_data() {
948 $this->setUser($this->user
);
949 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
950 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
951 $cm = get_coursemodule_from_instance('forum', $forum->id
);
952 $c = new completion_info($this->course
);
954 // 1) Test with new data.
955 $data = new stdClass();
957 $data->userid
= $this->user
->id
;
958 $data->coursemoduleid
= $cm->id
;
959 $data->completionstate
= COMPLETION_COMPLETE
;
960 $data->timemodified
= time();
961 $data->viewed
= COMPLETION_NOT_VIEWED
;
962 $data->overrideby
= null;
964 $c->internal_set_data($cm, $data);
965 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id
));
966 $this->assertEquals($d1, $data->id
);
967 $cache = cache
::make('core', 'completion');
968 // Cache was not set for another user.
969 $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
970 $this->assertEquals([
971 'cacherev' => $this->course
->cacherev
,
972 $cm->id
=> array_merge(
974 ['other_cm_completion_data_fetched' => true]
979 // 2) Test with existing data and for different user.
980 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
981 $cm2 = get_coursemodule_from_instance('forum', $forum2->id
);
982 $newuser = $this->getDataGenerator()->create_user();
984 $d2 = new stdClass();
986 $d2->userid
= $newuser->id
;
987 $d2->coursemoduleid
= $cm2->id
;
988 $d2->completionstate
= COMPLETION_COMPLETE
;
989 $d2->timemodified
= time();
990 $d2->viewed
= COMPLETION_NOT_VIEWED
;
991 $d2->overrideby
= null;
992 $c->internal_set_data($cm2, $d2);
993 // Cache for current user returns the data.
994 $cachevalue = $cache->get($data->userid
. '_' . $cm->course
);
995 $this->assertEquals(array_merge(
997 ['other_cm_completion_data_fetched' => true]
998 ), $cachevalue[$cm->id
]);
1000 // Cache for another user is not filled.
1001 $this->assertEquals(false, $cache->get($d2->userid
. '_' . $cm2->course
));
1003 // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1004 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
1005 $cm3 = get_coursemodule_from_instance('forum', $forum3->id
);
1006 $newuser2 = $this->getDataGenerator()->create_user();
1007 $d3 = new stdClass();
1009 $d3->userid
= $newuser2->id
;
1010 $d3->coursemoduleid
= $cm3->id
;
1011 $d3->completionstate
= COMPLETION_COMPLETE
;
1012 $d3->timemodified
= time();
1013 $d3->viewed
= COMPLETION_NOT_VIEWED
;
1014 $d3->overrideby
= null;
1015 $DB->insert_record('course_modules_completion', $d3);
1016 $c->internal_set_data($cm, $data);
1018 // 4) Test instant course completions.
1019 $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course
->id
),
1020 array('completion' => 1));
1021 $cm = get_coursemodule_from_instance('data', $dataactivity->id
);
1022 $c = new completion_info($this->course
);
1023 $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid
);
1025 // Add activity completion criteria.
1026 $criteriadata = new stdClass();
1027 $criteriadata->id
= $this->course
->id
;
1028 $criteriadata->criteria_activity
= array();
1030 $criteriadata->criteria_activity
[$cmdata->id
] = 1;
1031 $class = 'completion_criteria_activity';
1032 $criterion = new $class();
1033 $criterion->update_config($criteriadata);
1035 $actual = $DB->get_records('course_completions');
1036 $this->assertEmpty($actual);
1038 $data->coursemoduleid
= $cm->id
;
1039 $c->internal_set_data($cm, $data);
1040 $actual = $DB->get_records('course_completions');
1041 $this->assertEquals(1, count($actual));
1042 $this->assertEquals($this->user
->id
, reset($actual)->userid
);
1044 $data->userid
= $newuser2->id
;
1045 $c->internal_set_data($cm, $data, true);
1046 $actual = $DB->get_records('course_completions');
1047 $this->assertEquals(1, count($actual));
1048 $this->assertEquals($this->user
->id
, reset($actual)->userid
);
1052 * @covers ::get_progress_all
1054 public function test_get_progress_all_few() {
1056 $this->mock_setup();
1058 $mockbuilder = $this->getMockBuilder('completion_info');
1059 $mockbuilder->onlyMethods(array('get_tracked_users'));
1060 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1061 $c = $mockbuilder->getMock();
1063 // With few results.
1064 $c->expects($this->once())
1065 ->method('get_tracked_users')
1066 ->with(false, array(), 0, '', '', '', null)
1067 ->will($this->returnValue(array(
1068 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1069 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1070 $DB->expects($this->once())
1071 ->method('get_in_or_equal')
1072 ->with(array(100, 201))
1073 ->will($this->returnValue(array(' IN (100, 201)', array())));
1074 $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1075 $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1076 $DB->expects($this->once())
1077 ->method('get_recordset_sql')
1078 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1080 $this->assertEquals(array(
1081 100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1082 'progress' => array(13 => $progress1)),
1083 201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1084 'progress' => array(14 => $progress2)),
1085 ), $c->get_progress_all(false));
1089 * @covers ::get_progress_all
1091 public function test_get_progress_all_lots() {
1093 $this->mock_setup();
1095 $mockbuilder = $this->getMockBuilder('completion_info');
1096 $mockbuilder->onlyMethods(array('get_tracked_users'));
1097 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1098 $c = $mockbuilder->getMock();
1102 $progress = array();
1103 // With more than 1000 results.
1104 for ($i = 100; $i < 2000; $i++
) {
1105 $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1107 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1108 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1110 $c->expects($this->once())
1111 ->method('get_tracked_users')
1112 ->with(true, 3, 0, '', '', '', null)
1113 ->will($this->returnValue($tracked));
1114 $DB->expects($this->exactly(2))
1115 ->method('get_in_or_equal')
1117 array(array_slice($ids, 0, 1000)),
1118 array(array_slice($ids, 1000))
1120 ->willReturnOnConsecutiveCalls(
1121 array(' IN whatever', array()),
1122 array(' IN whatever2', array()));
1123 $DB->expects($this->exactly(2))
1124 ->method('get_recordset_sql')
1125 ->willReturnOnConsecutiveCalls(
1126 new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1127 new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1129 $result = $c->get_progress_all(true, 3);
1131 $resultok = $resultok && ($ids == array_keys($result));
1133 foreach ($result as $userid => $data) {
1134 $resultok = $resultok && $data->firstname
== 'frog';
1135 $resultok = $resultok && $data->lastname
== $userid;
1136 $resultok = $resultok && $data->id
== $userid;
1137 $cms = $data->progress
;
1138 $resultok = $resultok && (array(13, 14) == array_keys($cms));
1139 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1140 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1142 $this->assertTrue($resultok);
1143 $this->assertCount(count($tracked), $result);
1147 * @covers ::inform_grade_changed
1149 public function test_inform_grade_changed() {
1150 $this->mock_setup();
1152 $mockbuilder = $this->getMockBuilder('completion_info');
1153 $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1154 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1156 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1157 $item = (object)array('itemnumber' => 3, 'gradepass' => 1, 'hidden' => 0);
1158 $grade = (object)array('userid' => 31337, 'finalgrade' => 0, 'rawgrade' => 0);
1160 // Not enabled (should do nothing).
1161 $c = $mockbuilder->getMock();
1162 $c->expects($this->once())
1163 ->method('is_enabled')
1165 ->will($this->returnValue(false));
1166 $c->inform_grade_changed($cm, $item, $grade, false);
1168 // Enabled but still no grade completion required, should still do nothing.
1169 $c = $mockbuilder->getMock();
1170 $c->expects($this->once())
1171 ->method('is_enabled')
1173 ->will($this->returnValue(true));
1174 $c->inform_grade_changed($cm, $item, $grade, false);
1176 // Enabled and completion required but item number is wrong, does nothing.
1177 $c = $mockbuilder->getMock();
1178 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1179 $c->expects($this->once())
1180 ->method('is_enabled')
1182 ->will($this->returnValue(true));
1183 $c->inform_grade_changed($cm, $item, $grade, false);
1185 // Enabled and completion required and item number right. It is supposed
1186 // to call update_state with the new potential state being obtained from
1187 // internal_get_grade_state.
1188 $c = $mockbuilder->getMock();
1189 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1190 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1191 $c->expects($this->once())
1192 ->method('is_enabled')
1194 ->will($this->returnValue(true));
1195 $c->expects($this->once())
1196 ->method('update_state')
1197 ->with($cm, COMPLETION_COMPLETE_PASS
, 31337)
1198 ->will($this->returnValue(true));
1199 $c->inform_grade_changed($cm, $item, $grade, false);
1201 // Same as above but marked deleted. It is supposed to call update_state
1202 // with new potential state being COMPLETION_INCOMPLETE.
1203 $c = $mockbuilder->getMock();
1204 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1205 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1206 $c->expects($this->once())
1207 ->method('is_enabled')
1209 ->will($this->returnValue(true));
1210 $c->expects($this->once())
1211 ->method('update_state')
1212 ->with($cm, COMPLETION_INCOMPLETE
, 31337)
1213 ->will($this->returnValue(true));
1214 $c->inform_grade_changed($cm, $item, $grade, true);
1218 * @covers ::internal_get_grade_state
1220 public function test_internal_get_grade_state() {
1221 $this->mock_setup();
1223 $item = new stdClass
;
1224 $grade = new stdClass
;
1226 $item->gradepass
= 4;
1228 $grade->rawgrade
= 4.0;
1229 $grade->finalgrade
= null;
1231 // Grade has pass mark and is not hidden, user passes.
1232 $this->assertEquals(
1233 COMPLETION_COMPLETE_PASS
,
1234 completion_info
::internal_get_grade_state($item, $grade));
1236 // Same but user fails.
1237 $grade->rawgrade
= 3.9;
1238 $this->assertEquals(
1239 COMPLETION_COMPLETE_FAIL
,
1240 completion_info
::internal_get_grade_state($item, $grade));
1242 // User fails on raw grade but passes on final.
1243 $grade->finalgrade
= 4.0;
1244 $this->assertEquals(
1245 COMPLETION_COMPLETE_PASS
,
1246 completion_info
::internal_get_grade_state($item, $grade));
1250 $this->assertEquals(
1251 COMPLETION_COMPLETE
,
1252 completion_info
::internal_get_grade_state($item, $grade));
1254 // Item isn't hidden but has no pass mark.
1256 $item->gradepass
= 0;
1257 $this->assertEquals(
1258 COMPLETION_COMPLETE
,
1259 completion_info
::internal_get_grade_state($item, $grade));
1263 * @test ::get_activities
1265 public function test_get_activities() {
1267 $this->resetAfterTest();
1269 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1270 $CFG->enablecompletion
= true;
1272 // Create a course with mixed auto completion data.
1273 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1274 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1275 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL
);
1276 $completionnone = array('completion' => COMPLETION_TRACKING_NONE
);
1277 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionauto);
1278 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id
), $completionauto);
1279 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id
), $completionmanual);
1281 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionnone);
1282 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id
), $completionnone);
1283 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id
), $completionnone);
1285 // Create data in another course to make sure it's not considered.
1286 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1287 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id
), $completionauto);
1288 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id
), $completionmanual);
1289 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id
), $completionnone);
1291 $c = new completion_info($course);
1292 $activities = $c->get_activities();
1293 $this->assertCount(3, $activities);
1294 $this->assertTrue(isset($activities[$forum->cmid
]));
1295 $this->assertSame($forum->name
, $activities[$forum->cmid
]->name
);
1296 $this->assertTrue(isset($activities[$page->cmid
]));
1297 $this->assertSame($page->name
, $activities[$page->cmid
]->name
);
1298 $this->assertTrue(isset($activities[$data->cmid
]));
1299 $this->assertSame($data->name
, $activities[$data->cmid
]->name
);
1301 $this->assertFalse(isset($activities[$forum2->cmid
]));
1302 $this->assertFalse(isset($activities[$page2->cmid
]));
1303 $this->assertFalse(isset($activities[$data2->cmid
]));
1307 * @test ::has_activities
1309 public function test_has_activities() {
1311 $this->resetAfterTest();
1313 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1314 $CFG->enablecompletion
= true;
1316 // Create a course with mixed auto completion data.
1317 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1318 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1319 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1320 $completionnone = array('completion' => COMPLETION_TRACKING_NONE
);
1321 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id
), $completionauto);
1322 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id
), $completionnone);
1324 $c1 = new completion_info($course);
1325 $c2 = new completion_info($course2);
1327 $this->assertTrue($c1->has_activities());
1328 $this->assertFalse($c2->has_activities());
1332 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1334 * @covers ::delete_course_completion_data
1335 * @covers ::delete_all_completion_data
1337 public function test_course_delete_prerequisite() {
1340 $this->setup_data();
1342 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1344 $criteriadata = (object) [
1345 'id' => $this->course
->id
,
1346 'criteria_course' => [$courseprerequisite->id
],
1349 /** @var completion_criteria_course $criteria */
1350 $criteria = completion_criteria
::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
]);
1351 $criteria->update_config($criteriadata);
1354 $this->assertTrue($DB->record_exists('course_completion_criteria', [
1355 'course' => $this->course
->id
,
1356 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
,
1357 'courseinstance' => $courseprerequisite->id
,
1360 // Deleting the prerequisite course should remove the completion criteria.
1361 delete_course($courseprerequisite, false);
1363 $this->assertFalse($DB->record_exists('course_completion_criteria', [
1364 'course' => $this->course
->id
,
1365 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE
,
1366 'courseinstance' => $courseprerequisite->id
,
1371 * Test course module completion update event.
1373 * @covers \core\event\course_module_completion_updated
1375 public function test_course_module_completion_updated_event() {
1378 $this->setup_data();
1380 $this->setAdminUser();
1382 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1383 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course
->id
), $completionauto);
1385 $c = new completion_info($this->course
);
1386 $activities = $c->get_activities();
1387 $this->assertEquals(1, count($activities));
1388 $this->assertTrue(isset($activities[$forum->cmid
]));
1389 $this->assertEquals($activities[$forum->cmid
]->name
, $forum->name
);
1391 $current = $c->get_data($activities[$forum->cmid
], false, $this->user
->id
);
1392 $current->completionstate
= COMPLETION_COMPLETE
;
1393 $current->timemodified
= time();
1394 $sink = $this->redirectEvents();
1395 $c->internal_set_data($activities[$forum->cmid
], $current);
1396 $events = $sink->get_events();
1397 $event = reset($events);
1398 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1399 $this->assertEquals($forum->cmid
,
1400 $event->get_record_snapshot('course_modules_completion', $event->objectid
)->coursemoduleid
);
1401 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid
));
1402 $this->assertEquals(context_module
::instance($forum->cmid
), $event->get_context());
1403 $this->assertEquals($USER->id
, $event->userid
);
1404 $this->assertEquals($this->user
->id
, $event->relateduserid
);
1405 $this->assertInstanceOf('moodle_url', $event->get_url());
1406 $this->assertEventLegacyData($current, $event);
1410 * Test course completed event.
1412 * @covers \core\event\course_completed
1414 public function test_course_completed_event() {
1417 $this->setup_data();
1418 $this->setAdminUser();
1420 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1421 $ccompletion = new completion_completion(array('course' => $this->course
->id
, 'userid' => $this->user
->id
));
1423 // Mark course as complete and get triggered event.
1424 $sink = $this->redirectEvents();
1425 $ccompletion->mark_complete();
1426 $events = $sink->get_events();
1427 $event = reset($events);
1429 $this->assertInstanceOf('\core\event\course_completed', $event);
1430 $this->assertEquals($this->course
->id
, $event->get_record_snapshot('course_completions', $event->objectid
)->course
);
1431 $this->assertEquals($this->course
->id
, $event->courseid
);
1432 $this->assertEquals($USER->id
, $event->userid
);
1433 $this->assertEquals($this->user
->id
, $event->relateduserid
);
1434 $this->assertEquals(context_course
::instance($this->course
->id
), $event->get_context());
1435 $this->assertInstanceOf('moodle_url', $event->get_url());
1436 $data = $ccompletion->get_record_data();
1437 $this->assertEventLegacyData($data, $event);
1441 * Test course completed message.
1443 * @covers \core\event\course_completed
1445 public function test_course_completed_message() {
1446 $this->setup_data();
1447 $this->setAdminUser();
1449 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC
);
1450 $ccompletion = new completion_completion(array('course' => $this->course
->id
, 'userid' => $this->user
->id
));
1452 // Mark course as complete and get the message.
1453 $sink = $this->redirectMessages();
1454 $ccompletion->mark_complete();
1455 $messages = $sink->get_messages();
1458 $this->assertCount(1, $messages);
1459 $message = array_pop($messages);
1461 $this->assertEquals(core_user
::get_noreply_user()->id
, $message->useridfrom
);
1462 $this->assertEquals($this->user
->id
, $message->useridto
);
1463 $this->assertEquals('coursecompleted', $message->eventtype
);
1464 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject
);
1465 $this->assertStringContainsString($this->course
->fullname
, $message->fullmessage
);
1469 * Test course completed event.
1471 * @covers \core\event\course_completion_updated
1473 public function test_course_completion_updated_event() {
1474 $this->setup_data();
1475 $coursecontext = context_course
::instance($this->course
->id
);
1476 $coursecompletionevent = \core\event\course_completion_updated
::create(
1478 'courseid' => $this->course
->id
,
1479 'context' => $coursecontext
1483 // Mark course as complete and get triggered event.
1484 $sink = $this->redirectEvents();
1485 $coursecompletionevent->trigger();
1486 $events = $sink->get_events();
1487 $event = array_pop($events);
1490 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1491 $this->assertEquals($this->course
->id
, $event->courseid
);
1492 $this->assertEquals($coursecontext, $event->get_context());
1493 $this->assertInstanceOf('moodle_url', $event->get_url());
1494 $expectedlegacylog = array($this->course
->id
, 'course', 'completion updated', 'completion.php?id='.$this->course
->id
);
1495 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1499 * @covers \completion_can_view_data
1501 public function test_completion_can_view_data() {
1502 $this->setup_data();
1504 $student = $this->getDataGenerator()->create_user();
1505 $this->getDataGenerator()->enrol_user($student->id
, $this->course
->id
);
1507 $this->setUser($student);
1508 $this->assertTrue(completion_can_view_data($student->id
, $this->course
->id
));
1509 $this->assertFalse(completion_can_view_data($this->user
->id
, $this->course
->id
));
1513 * Data provider for test_get_grade_completion().
1517 public function get_grade_completion_provider() {
1519 'Grade not required' => [false, false, null, null, null],
1520 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE
],
1521 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE
],
1522 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS
],
1523 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL
],
1528 * Test for \completion_info::get_grade_completion().
1530 * @dataProvider get_grade_completion_provider
1531 * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1532 * @param bool $hasgrade Whether to set grade for the user in this activity.
1533 * @param int|null $passinggrade Passing grade to set for the test activity.
1534 * @param string|null $expectedexception Expected exception.
1535 * @param int|null $expectedresult The expected completion status.
1536 * @covers ::get_grade_completion
1538 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?
int $passinggrade,
1539 ?
string $expectedexception, ?
int $expectedresult) {
1540 $this->setup_data();
1542 /** @var \mod_assign_generator $assigngenerator */
1543 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1544 $assign = $assigngenerator->create_instance([
1545 'course' => $this->course
->id
,
1546 'completion' => COMPLETION_ENABLED
,
1547 'completionusegrade' => $completionusegrade,
1548 'gradepass' => $passinggrade,
1551 $cm = cm_info
::create(get_coursemodule_from_instance('assign', $assign->id
));
1552 if ($completionusegrade && $hasgrade) {
1553 $assigninstance = new assign($cm->context
, $cm, $this->course
);
1554 $grade = $assigninstance->get_user_grade($this->user
->id
, true);
1556 $assigninstance->update_grade($grade);
1559 $completioninfo = new completion_info($this->course
);
1560 if ($expectedexception) {
1561 $this->expectException($expectedexception);
1563 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user
->id
);
1564 $this->assertEquals($expectedresult, $gradecompletion);
1568 * Test the return value for cases when the activity module does not have associated grade_item.
1570 * @covers ::get_grade_completion
1572 public function test_get_grade_completion_without_grade_item() {
1575 $this->setup_data();
1577 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1578 'course' => $this->course
->id
,
1579 'completion' => COMPLETION_ENABLED
,
1580 'completionusegrade' => true,
1584 $cm = cm_info
::create(get_coursemodule_from_instance('assign', $assign->id
));
1586 $DB->delete_records('grade_items', [
1587 'courseid' => $this->course
->id
,
1588 'itemtype' => 'mod',
1589 'itemmodule' => 'assign',
1590 'iteminstance' => $assign->id
,
1593 // Without the grade_item, the activity is considered incomplete.
1594 $completioninfo = new completion_info($this->course
);
1595 $this->assertEquals(COMPLETION_INCOMPLETE
, $completioninfo->get_grade_completion($cm, $this->user
->id
));
1597 // Once the activity is graded, the grade_item is automatically created.
1598 $assigninstance = new assign($cm->context
, $cm, $this->course
);
1599 $grade = $assigninstance->get_user_grade($this->user
->id
, true);
1601 $assigninstance->update_grade($grade);
1603 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1604 $this->assertEquals(COMPLETION_COMPLETE
, $completioninfo->get_grade_completion($cm, $this->user
->id
));
1608 * Test for aggregate_completions().
1610 * @covers \aggregate_completions
1612 public function test_aggregate_completions() {
1614 $this->resetAfterTest(true);
1617 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1619 for ($i = 0; $i < 4; $i++
) {
1620 $students[] = $this->getDataGenerator()->create_user();
1623 $teacher = $this->getDataGenerator()->create_user();
1624 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1625 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1627 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1628 foreach ($students as $student) {
1629 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1632 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id
),
1633 array('completion' => 1));
1634 $cmdata = get_coursemodule_from_id('data', $data->cmid
);
1636 // Add activity completion criteria.
1637 $criteriadata = new stdClass();
1638 $criteriadata->id
= $course->id
;
1639 $criteriadata->criteria_activity
= array();
1641 $criteriadata->criteria_activity
[$cmdata->id
] = 1;
1642 $class = 'completion_criteria_activity';
1643 $criterion = new $class();
1644 $criterion->update_config($criteriadata);
1646 $this->setUser($teacher);
1648 // Mark activity complete for both students.
1649 $cm = get_coursemodule_from_instance('data', $data->id
);
1650 $completioncriteria = $DB->get_record('course_completion_criteria', []);
1651 foreach ($students as $student) {
1652 $cmcompletionrecords[] = (object)[
1653 'coursemoduleid' => $cm->id
,
1654 'userid' => $student->id
,
1655 'completionstate' => 1,
1657 'overrideby' => null,
1658 'timemodified' => 0,
1661 $usercompletions[] = (object)[
1662 'criteriaid' => $completioncriteria->id
,
1663 'userid' => $student->id
,
1664 'timecompleted' => $time,
1668 'course' => $course->id
,
1669 'userid' => $student->id
1671 $ccompletion = new completion_completion($cc);
1672 $completion[] = $ccompletion->mark_inprogress($time);
1674 $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1675 $DB->insert_records('course_completion_crit_compl', $usercompletions);
1677 // MDL-33320: for instant completions we need aggregate to work in a single run.
1678 $DB->set_field('course_completions', 'reaggregate', $time - 2);
1680 foreach ($students as $student) {
1681 $result = $DB->get_record('course_completions', ['userid' => $student->id
, 'reaggregate' => 0]);
1682 $this->assertFalse($result);
1685 aggregate_completions($completion[0]);
1687 $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id
, 'reaggregate' => 0]);
1688 $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id
, 'reaggregate' => 0]);
1689 $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id
, 'reaggregate' => 0]);
1691 $this->assertIsObject($result1);
1692 $this->assertFalse($result2);
1693 $this->assertFalse($result3);
1695 aggregate_completions(0);
1697 foreach ($students as $student) {
1698 $result = $DB->get_record('course_completions', ['userid' => $student->id
, 'reaggregate' => 0]);
1699 $this->assertIsObject($result);
1704 * Test for completion_completion::_save().
1706 * @covers \completion_completion::_save
1708 public function test_save() {
1710 $this->resetAfterTest(true);
1712 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1714 $student = $this->getDataGenerator()->create_user();
1715 $teacher = $this->getDataGenerator()->create_user();
1716 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1717 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1719 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1720 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1722 $this->setUser($teacher);
1725 'course' => $course->id
,
1726 'userid' => $student->id
1728 $ccompletion = new completion_completion($cc);
1730 $completions = $DB->get_records('course_completions');
1731 $this->assertEmpty($completions);
1733 // We're testing a private method, so we need to setup reflector magic.
1734 $method = new ReflectionMethod($ccompletion, '_save');
1735 $method->setAccessible(true); // Allow accessing of private method.
1736 $completionid = $method->invoke($ccompletion);
1737 $completions = $DB->get_records('course_completions');
1738 $this->assertEquals(count($completions), 1);
1739 $this->assertEquals(reset($completions)->id
, $completionid);
1741 $ccompletion->id
= 0;
1742 $method = new ReflectionMethod($ccompletion, '_save');
1743 $method->setAccessible(true); // Allow accessing of private method.
1744 $completionid = $method->invoke($ccompletion);
1745 $this->assertDebuggingCalled('Can not update data object, no id!');
1746 $this->assertNull($completionid);
1750 * Test for completion_completion::mark_enrolled().
1752 * @covers \completion_completion::mark_enrolled
1754 public function test_mark_enrolled() {
1756 $this->resetAfterTest(true);
1758 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1760 $student = $this->getDataGenerator()->create_user();
1761 $teacher = $this->getDataGenerator()->create_user();
1762 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1763 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1765 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1766 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1768 $this->setUser($teacher);
1771 'course' => $course->id
,
1772 'userid' => $student->id
1774 $ccompletion = new completion_completion($cc);
1776 $completions = $DB->get_records('course_completions');
1777 $this->assertEmpty($completions);
1779 $completionid = $ccompletion->mark_enrolled();
1780 $completions = $DB->get_records('course_completions');
1781 $this->assertEquals(count($completions), 1);
1782 $this->assertEquals(reset($completions)->id
, $completionid);
1784 $ccompletion->id
= 0;
1785 $completionid = $ccompletion->mark_enrolled();
1786 $this->assertDebuggingCalled('Can not update data object, no id!');
1787 $this->assertNull($completionid);
1788 $completions = $DB->get_records('course_completions');
1789 $this->assertEquals(1, count($completions));
1793 * Test for completion_completion::mark_inprogress().
1795 * @covers \completion_completion::mark_inprogress
1797 public function test_mark_inprogress() {
1799 $this->resetAfterTest(true);
1801 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1803 $student = $this->getDataGenerator()->create_user();
1804 $teacher = $this->getDataGenerator()->create_user();
1805 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1806 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1808 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1809 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1811 $this->setUser($teacher);
1814 'course' => $course->id
,
1815 'userid' => $student->id
1817 $ccompletion = new completion_completion($cc);
1819 $completions = $DB->get_records('course_completions');
1820 $this->assertEmpty($completions);
1822 $completionid = $ccompletion->mark_inprogress();
1823 $completions = $DB->get_records('course_completions');
1824 $this->assertEquals(1, count($completions));
1825 $this->assertEquals(reset($completions)->id
, $completionid);
1827 $ccompletion->id
= 0;
1828 $completionid = $ccompletion->mark_inprogress();
1829 $this->assertDebuggingCalled('Can not update data object, no id!');
1830 $this->assertNull($completionid);
1831 $completions = $DB->get_records('course_completions');
1832 $this->assertEquals(1, count($completions));
1836 * Test for completion_completion::mark_complete().
1838 * @covers \completion_completion::mark_complete
1840 public function test_mark_complete() {
1842 $this->resetAfterTest(true);
1844 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1846 $student = $this->getDataGenerator()->create_user();
1847 $teacher = $this->getDataGenerator()->create_user();
1848 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1849 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1851 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1852 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1854 $this->setUser($teacher);
1857 'course' => $course->id
,
1858 'userid' => $student->id
1860 $ccompletion = new completion_completion($cc);
1862 $completions = $DB->get_records('course_completions');
1863 $this->assertEmpty($completions);
1865 $completionid = $ccompletion->mark_complete();
1866 $completions = $DB->get_records('course_completions');
1867 $this->assertEquals(1, count($completions));
1868 $this->assertEquals(reset($completions)->id
, $completionid);
1870 $ccompletion->id
= 0;
1871 $completionid = $ccompletion->mark_complete();
1872 $this->assertNull($completionid);
1873 $completions = $DB->get_records('course_completions');
1874 $this->assertEquals(1, count($completions));
1878 * Test for completion_criteria_completion::mark_complete().
1880 * @covers \completion_criteria_completion::mark_complete
1882 public function test_criteria_mark_complete() {
1884 $this->resetAfterTest(true);
1886 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1888 $student = $this->getDataGenerator()->create_user();
1889 $teacher = $this->getDataGenerator()->create_user();
1890 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1891 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1893 $this->getDataGenerator()->enrol_user($teacher->id
, $course->id
, $teacherrole->id
);
1894 $this->getDataGenerator()->enrol_user($student->id
, $course->id
, $studentrole->id
);
1896 $this->setUser($teacher);
1899 'course' => $course->id
,
1901 'userid' => $student->id
,
1902 'timecompleted' => time()
1904 $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY
);
1906 $completions = $DB->get_records('course_completions');
1907 $this->assertEmpty($completions);
1909 $completionid = $completion->mark_complete($record['timecompleted']);
1910 $completions = $DB->get_records('course_completions');
1911 $this->assertEquals(1, count($completions));
1912 $this->assertEquals(reset($completions)->id
, $completionid);
1916 class core_completionlib_fake_recordset
implements Iterator
{
1918 protected $values, $index;
1920 public function __construct($values) {
1921 $this->values
= $values;
1925 public function current() {
1926 return $this->values
[$this->index
];
1929 public function key() {
1930 return $this->values
[$this->index
];
1933 public function next() {
1937 public function rewind() {
1941 public function valid() {
1942 return count($this->values
) > $this->index
;
1945 public function close() {
1946 $this->closed
= true;
1949 public function was_closed() {
1950 return $this->closed
;