MDL-70815 completion: Test internal_get_state() with custom completion
[moodle.git] / lib / tests / completionlib_test.php
CommitLineData
4059b645
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
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/>.
16
17/**
82ff8e29 18 * Completion tests.
4059b645
PS
19 *
20 * @package core_completion
21 * @category phpunit
22 * @copyright 2008 Sam Marshall
82ff8e29 23 * @copyright 2013 Frédéric Massart
4059b645
PS
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30require_once($CFG->libdir.'/completionlib.php');
31
82ff8e29 32class core_completionlib_testcase extends advanced_testcase {
cdc54199
RT
33 protected $course;
34 protected $user;
35 protected $module1;
36 protected $module2;
37
82ff8e29
PS
38 protected function mock_setup() {
39 global $DB, $CFG, $USER;
4059b645 40
82ff8e29 41 $this->resetAfterTest();
4059b645 42
52f3e060 43 $DB = $this->createMock(get_class($DB));
4059b645 44 $CFG->enablecompletion = COMPLETION_ENABLED;
4059b645
PS
45 $USER = (object)array('id' =>314159);
46 }
47
cdc54199
RT
48 /**
49 * Create course with user and activities.
50 */
51 protected function setup_data() {
52 global $DB, $CFG;
53
54 $this->resetAfterTest();
55
7fbe33fc
MG
56 // Enable completion before creating modules, otherwise the completion data is not written in DB.
57 $CFG->enablecompletion = true;
58
cdc54199 59 // Create a course with activities.
7fbe33fc 60 $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
cdc54199 61 $this->user = $this->getDataGenerator()->create_user();
ef5df7b7 62 $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
cdc54199
RT
63
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));
66 }
67
2a67e105
PS
68 /**
69 * Asserts that two variables are equal.
70 *
71 * @param mixed $expected
72 * @param mixed $actual
73 * @param string $message
74 * @param float $delta
75 * @param integer $maxDepth
76 * @param boolean $canonicalize
77 * @param boolean $ignoreCase
78 */
6bcc5dfd
JP
79 public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
80 bool $canonicalize = false, bool $ignoreCase = false): void {
2a67e105
PS
81 // Nasty cheating hack: prevent random failures on timemodified field.
82 if (is_object($expected) and is_object($actual)) {
83 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
84 if ($expected->timemodified + 1 == $actual->timemodified) {
85 $expected = clone($expected);
86 $expected->timemodified = $actual->timemodified;
87 }
88 }
89 }
90 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
91 }
92
82ff8e29 93 public function test_is_enabled() {
4059b645 94 global $CFG;
82ff8e29 95 $this->mock_setup();
4059b645 96
82ff8e29 97 // Config alone.
4059b645
PS
98 $CFG->enablecompletion = COMPLETION_DISABLED;
99 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
100 $CFG->enablecompletion = COMPLETION_ENABLED;
101 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
102
82ff8e29 103 // Course.
4059b645
PS
104 $course = (object)array('id' =>13);
105 $c = new completion_info($course);
106 $course->enablecompletion = COMPLETION_DISABLED;
107 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
108 $course->enablecompletion = COMPLETION_ENABLED;
109 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
110 $CFG->enablecompletion = COMPLETION_DISABLED;
111 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
112
82ff8e29 113 // Course and CM.
4059b645
PS
114 $cm = new stdClass();
115 $cm->completion = COMPLETION_TRACKING_MANUAL;
116 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
117 $CFG->enablecompletion = COMPLETION_ENABLED;
118 $course->enablecompletion = COMPLETION_DISABLED;
119 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
120 $course->enablecompletion = COMPLETION_ENABLED;
121 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
122 $cm->completion = COMPLETION_TRACKING_NONE;
123 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
124 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
125 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
126 }
127
82ff8e29
PS
128 public function test_update_state() {
129 $this->mock_setup();
4059b645 130
52f3e060 131 $mockbuilder = $this->getMockBuilder('completion_info');
8fbc41d8
JD
132 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
133 'user_can_override_completion'));
52f3e060
RT
134 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
135 $c = $mockbuilder->getMock();
4059b645
PS
136 $cm = (object)array('id'=>13, 'course'=>42);
137
82ff8e29 138 // Not enabled, should do nothing.
4059b645
PS
139 $c->expects($this->at(0))
140 ->method('is_enabled')
141 ->with($cm)
142 ->will($this->returnValue(false));
143 $c->update_state($cm);
144
82ff8e29 145 // Enabled, but current state is same as possible result, do nothing.
60a6b36c 146 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
4059b645
PS
147 $c->expects($this->at(0))
148 ->method('is_enabled')
149 ->with($cm)
150 ->will($this->returnValue(true));
151 $c->expects($this->at(1))
152 ->method('get_data')
153 ->with($cm, false, 0)
154 ->will($this->returnValue($current));
155 $c->update_state($cm, COMPLETION_COMPLETE);
156
157 // Enabled, but current state is a specific one and new state is just
82ff8e29 158 // complete, so do nothing.
4059b645
PS
159 $current->completionstate = COMPLETION_COMPLETE_PASS;
160 $c->expects($this->at(0))
161 ->method('is_enabled')
162 ->with($cm)
163 ->will($this->returnValue(true));
164 $c->expects($this->at(1))
165 ->method('get_data')
166 ->with($cm, false, 0)
167 ->will($this->returnValue($current));
168 $c->update_state($cm, COMPLETION_COMPLETE);
169
82ff8e29
PS
170 // Manual, change state (no change).
171 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
4059b645
PS
172 $current->completionstate=COMPLETION_COMPLETE;
173 $c->expects($this->at(0))
174 ->method('is_enabled')
175 ->with($cm)
176 ->will($this->returnValue(true));
177 $c->expects($this->at(1))
178 ->method('get_data')
179 ->with($cm, false, 0)
180 ->will($this->returnValue($current));
181 $c->update_state($cm, COMPLETION_COMPLETE);
182
82ff8e29 183 // Manual, change state (change).
4059b645
PS
184 $c->expects($this->at(0))
185 ->method('is_enabled')
186 ->with($cm)
187 ->will($this->returnValue(true));
188 $c->expects($this->at(1))
189 ->method('get_data')
190 ->with($cm, false, 0)
191 ->will($this->returnValue($current));
192 $changed = clone($current);
193 $changed->timemodified = time();
194 $changed->completionstate = COMPLETION_INCOMPLETE;
97b1a482
AN
195 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
196 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
4059b645
PS
197 $c->expects($this->at(2))
198 ->method('internal_set_data')
97b1a482 199 ->with($cm, $comparewith);
4059b645
PS
200 $c->update_state($cm, COMPLETION_INCOMPLETE);
201
82ff8e29
PS
202 // Auto, change state.
203 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
60a6b36c 204 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
4059b645
PS
205 $c->expects($this->at(0))
206 ->method('is_enabled')
207 ->with($cm)
208 ->will($this->returnValue(true));
209 $c->expects($this->at(1))
210 ->method('get_data')
211 ->with($cm, false, 0)
212 ->will($this->returnValue($current));
213 $c->expects($this->at(2))
214 ->method('internal_get_state')
215 ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
216 $changed = clone($current);
217 $changed->timemodified = time();
218 $changed->completionstate = COMPLETION_COMPLETE_PASS;
97b1a482
AN
219 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
220 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
4059b645
PS
221 $c->expects($this->at(3))
222 ->method('internal_set_data')
97b1a482 223 ->with($cm, $comparewith);
4059b645 224 $c->update_state($cm, COMPLETION_COMPLETE_PASS);
60a6b36c
EEAK
225
226 // Manual tracking, change state by overriding it manually.
227 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
228 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
229 $c->expects($this->at(0))
230 ->method('is_enabled')
231 ->with($cm)
232 ->will($this->returnValue(true));
8fbc41d8
JD
233 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
234 ->method('user_can_override_completion')
235 ->will($this->returnValue(true));
236 $c->expects($this->at(2))
60a6b36c
EEAK
237 ->method('get_data')
238 ->with($cm, false, 100)
239 ->will($this->returnValue($current));
240 $changed = clone($current);
241 $changed->timemodified = time();
242 $changed->completionstate = COMPLETION_COMPLETE;
243 $changed->overrideby = 314159;
244 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
245 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
8fbc41d8 246 $c->expects($this->at(3))
60a6b36c
EEAK
247 ->method('internal_set_data')
248 ->with($cm, $comparewith);
249 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
86f359bf
JD
250 // And confirm that the status can be changed back to incomplete without an override.
251 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
252 $c->expects($this->at(0))
253 ->method('get_data')
254 ->with($cm, false, 100)
255 ->will($this->returnValue($current));
256 $c->get_data($cm, false, 100);
60a6b36c 257
86f359bf 258 // Auto, change state via override, incomplete to complete.
60a6b36c
EEAK
259 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
260 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
261 $c->expects($this->at(0))
262 ->method('is_enabled')
263 ->with($cm)
264 ->will($this->returnValue(true));
8fbc41d8
JD
265 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
266 ->method('user_can_override_completion')
267 ->will($this->returnValue(true));
268 $c->expects($this->at(2))
60a6b36c
EEAK
269 ->method('get_data')
270 ->with($cm, false, 100)
271 ->will($this->returnValue($current));
272 $changed = clone($current);
273 $changed->timemodified = time();
274 $changed->completionstate = COMPLETION_COMPLETE;
275 $changed->overrideby = 314159;
276 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
277 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
8fbc41d8 278 $c->expects($this->at(3))
60a6b36c
EEAK
279 ->method('internal_set_data')
280 ->with($cm, $comparewith);
281 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
86f359bf
JD
282 $c->expects($this->at(0))
283 ->method('get_data')
284 ->with($cm, false, 100)
285 ->will($this->returnValue($changed));
286 $c->get_data($cm, false, 100);
60a6b36c 287
86f359bf
JD
288 // Now confirm that the status cannot be changed back to incomplete without an override.
289 // I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden.
290 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
291 $c->expects($this->at(0))
292 ->method('get_data')
293 ->with($cm, false, 100)
294 ->will($this->returnValue($changed));
295 $c->get_data($cm, false, 100);
296
297 // Now confirm the status can be changed back from complete to incomplete using an override.
60a6b36c
EEAK
298 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
299 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
300 $c->expects($this->at(0))
301 ->method('is_enabled')
302 ->with($cm)
303 ->will($this->returnValue(true));
8fbc41d8
JD
304 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
305 ->method('user_can_override_completion')
306 ->will($this->returnValue(true));
307 $c->expects($this->at(2))
60a6b36c 308 ->method('get_data')
86f359bf 309 ->with($cm, false, 100)
60a6b36c 310 ->will($this->returnValue($current));
86f359bf
JD
311 $changed = clone($current);
312 $changed->timemodified = time();
313 $changed->completionstate = COMPLETION_INCOMPLETE;
314 $changed->overrideby = 314159;
315 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
316 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
8fbc41d8 317 $c->expects($this->at(3))
86f359bf
JD
318 ->method('internal_set_data')
319 ->with($cm, $comparewith);
320 $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
321 $c->expects($this->at(0))
322 ->method('get_data')
323 ->with($cm, false, 100)
324 ->will($this->returnValue($changed));
325 $c->get_data($cm, false, 100);
4059b645
PS
326 }
327
23603315
JP
328 /**
329 * Data provider for test_internal_get_state().
330 *
331 * @return array[]
332 */
333 public function internal_get_state_provider() {
334 return [
335 'View required, but not viewed yet' => [
336 COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
337 ],
338 'View not required and not viewed yet' => [
339 COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
340 ],
341 'View not required, grade required but no grade yet, $cm->modname not set' => [
342 COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
343 ],
344 'View not required, grade required but no grade yet, $cm->course not set' => [
345 COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
346 ],
347 'View not required, grade not required' => [
348 COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
349 ],
350 ];
351 }
52f3e060 352
23603315
JP
353 /**
354 * Test for completion_info::get_state().
355 *
356 * @dataProvider internal_get_state_provider
357 * @param int $completionview
358 * @param int $completionusegrade
359 * @param string $unsetfield
360 * @param int $expectedstate
361 */
362 public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
363 $this->setup_data();
4059b645 364
23603315
JP
365 /** @var \mod_assign_generator $assigngenerator */
366 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
367 $assign = $assigngenerator->create_instance([
368 'course' => $this->course->id,
369 'completion' => COMPLETION_ENABLED,
370 'completionview' => $completionview,
371 'completionusegrade' => $completionusegrade,
372 ]);
373
374 $userid = $this->user->id;
375 $this->setUser($userid);
376
377 $cm = get_coursemodule_from_instance('assign', $assign->id);
378 if ($unsetfield) {
379 unset($cm->$unsetfield);
380 }
82ff8e29 381 // If view is required, but they haven't viewed it yet.
23603315 382 $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
4059b645 383
23603315
JP
384 $completioninfo = new completion_info($this->course);
385 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
386 }
4059b645 387
23603315
JP
388 /**
389 * Covers the case where internal_get_state() is being called for a user different from the logged in user.
390 */
391 public function test_internal_get_state_with_different_user() {
392 $this->setup_data();
4059b645 393
23603315
JP
394 /** @var \mod_assign_generator $assigngenerator */
395 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
396 $assign = $assigngenerator->create_instance([
397 'course' => $this->course->id,
398 'completion' => COMPLETION_ENABLED,
399 'completionusegrade' => 1,
400 ]);
401
402 $userid = $this->user->id;
403
404 $cm = get_coursemodule_from_instance('assign', $assign->id);
405 $usercm = cm_info::create($cm, $userid);
406
407 // Create a teacher account.
408 $teacher = $this->getDataGenerator()->create_user();
409 $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
410 // Log in as the teacher.
411 $this->setUser($teacher);
412
413 // Grade the student for this assignment.
414 $assign = new assign($usercm->context, $cm, $cm->course);
415 $data = (object)[
416 'sendstudentnotifications' => false,
417 'attemptnumber' => 1,
418 'grade' => 90,
419 ];
420 $assign->save_grade($userid, $data);
421
422 // The target user already received a grade, so internal_get_state should be already complete.
423 $completioninfo = new completion_info($this->course);
424 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
425
426 // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
427 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
4059b645
PS
428 }
429
8d29653f
JP
430 /**
431 * Test for internal_get_state() for an activity that supports custom completion.
432 */
433 public function test_internal_get_state_with_custom_completion() {
434 $this->setup_data();
435
436 $choicerecord = [
437 'course' => $this->course,
438 'completion' => COMPLETION_TRACKING_AUTOMATIC,
439 'completionsubmit' => COMPLETION_ENABLED,
440 ];
441 $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
442 $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
443
444 $completioninfo = new completion_info($this->course);
445
446 // Fetch completion for the user who hasn't made a choice yet.
447 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
448 $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
449
450 // Have the user make a choice.
451 $choicewithoptions = choice_get_choice($choice->id);
452 $optionids = array_keys($choicewithoptions->option);
453 choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
454 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
455 $this->assertEquals(COMPLETION_COMPLETE, $completion);
456 }
457
82ff8e29
PS
458 public function test_set_module_viewed() {
459 $this->mock_setup();
4059b645 460
52f3e060
RT
461 $mockbuilder = $this->getMockBuilder('completion_info');
462 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
463 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
464 $c = $mockbuilder->getMock();
4059b645
PS
465 $cm = (object)array('id'=>13, 'course'=>42);
466
82ff8e29 467 // Not tracking completion, should do nothing.
4059b645
PS
468 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
469 $c->set_module_viewed($cm);
470
82ff8e29 471 // Tracking completion but completion is disabled, should do nothing.
4059b645
PS
472 $cm->completionview = COMPLETION_VIEW_REQUIRED;
473 $c->expects($this->at(0))
474 ->method('is_enabled')
475 ->with($cm)
476 ->will($this->returnValue(false));
477 $c->set_module_viewed($cm);
478
479 // Now it's enabled, we expect it to get data. If data already has
82ff8e29 480 // viewed, still do nothing.
4059b645
PS
481 $c->expects($this->at(0))
482 ->method('is_enabled')
483 ->with($cm)
484 ->will($this->returnValue(true));
485 $c->expects($this->at(1))
486 ->method('get_data')
487 ->with($cm, 0)
488 ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
489 $c->set_module_viewed($cm);
490
491 // OK finally one that hasn't been viewed, now it should set it viewed
82ff8e29 492 // and update state.
4059b645
PS
493 $c->expects($this->at(0))
494 ->method('is_enabled')
495 ->with($cm)
496 ->will($this->returnValue(true));
497 $c->expects($this->at(1))
498 ->method('get_data')
1caeb4b4 499 ->with($cm, false, 1337)
4059b645
PS
500 ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
501 $c->expects($this->at(2))
502 ->method('internal_set_data')
503 ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
504 $c->expects($this->at(3))
505 ->method('update_state')
506 ->with($cm, COMPLETION_COMPLETE, 1337);
507 $c->set_module_viewed($cm, 1337);
508 }
509
82ff8e29 510 public function test_count_user_data() {
4059b645 511 global $DB;
82ff8e29 512 $this->mock_setup();
4059b645
PS
513
514 $course = (object)array('id'=>13);
515 $cm = (object)array('id'=>42);
516
517 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
518 $DB->expects($this->at(0))
519 ->method('get_field_sql')
520 ->will($this->returnValue(666));
521
4059b645
PS
522 $c = new completion_info($course);
523 $this->assertEquals(666, $c->count_user_data($cm));
524 }
525
82ff8e29 526 public function test_delete_all_state() {
3871db0a 527 global $DB;
82ff8e29 528 $this->mock_setup();
4059b645
PS
529
530 $course = (object)array('id'=>13);
82ff8e29 531 $cm = (object)array('id'=>42, 'course'=>13);
4059b645
PS
532 $c = new completion_info($course);
533
82ff8e29 534 // Check it works ok without data in session.
4059b645
PS
535 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
536 $DB->expects($this->at(0))
537 ->method('delete_records')
538 ->with('course_modules_completion', array('coursemoduleid'=>42))
539 ->will($this->returnValue(true));
540 $c->delete_all_state($cm);
4059b645
PS
541 }
542
82ff8e29 543 public function test_reset_all_state() {
4059b645 544 global $DB;
82ff8e29 545 $this->mock_setup();
4059b645 546
52f3e060
RT
547 $mockbuilder = $this->getMockBuilder('completion_info');
548 $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
549 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
550 $c = $mockbuilder->getMock();
4059b645
PS
551
552 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
553
554 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
555 $DB->expects($this->at(0))
556 ->method('get_recordset')
557 ->will($this->returnValue(
82ff8e29 558 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
4059b645
PS
559
560 $c->expects($this->at(0))
561 ->method('delete_all_state')
562 ->with($cm);
563
564 $c->expects($this->at(1))
565 ->method('get_tracked_users')
566 ->will($this->returnValue(array(
82ff8e29
PS
567 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
568 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
4059b645
PS
569
570 $c->expects($this->at(2))
571 ->method('update_state')
82ff8e29 572 ->with($cm, COMPLETION_UNKNOWN, 100);
4059b645
PS
573 $c->expects($this->at(3))
574 ->method('update_state')
82ff8e29 575 ->with($cm, COMPLETION_UNKNOWN, 101);
4059b645
PS
576 $c->expects($this->at(4))
577 ->method('update_state')
82ff8e29 578 ->with($cm, COMPLETION_UNKNOWN, 201);
4059b645
PS
579
580 $c->reset_all_state($cm);
581 }
582
23603315
JP
583 /**
584 * Data provider for test_get_data().
585 *
586 * @return array[]
587 */
588 public function get_data_provider() {
589 return [
590 'No completion record' => [
591 false, false, false, COMPLETION_INCOMPLETE
592 ],
593 'Not completed' => [
594 false, false, true, COMPLETION_INCOMPLETE
595 ],
596 'Completed' => [
597 false, false, true, COMPLETION_COMPLETE
598 ],
599 'Whole course, complete' => [
600 true, false, true, COMPLETION_COMPLETE
601 ],
602 'Get data for another user, result should be not cached' => [
603 false, true, true, COMPLETION_INCOMPLETE
604 ],
605 ];
606 }
607
608 /**
609 * Tests for completion_info::get_data().
610 *
611 * @dataProvider get_data_provider
612 * @param bool $wholecourse Whole course parameter for get_data().
613 * @param bool $sameuser Whether the user calling get_data() is the user itself.
614 * @param bool $hasrecord Whether to create a course_modules_completion record.
615 * @param int $completion The completion state expected.
616 */
617 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
3871db0a 618 global $DB;
4059b645 619
23603315
JP
620 $this->setup_data();
621 $user = $this->user;
622
623 /** @var \mod_choice_generator $choicegenerator */
624 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
625 $choice = $choicegenerator->create_instance([
626 'course' => $this->course->id,
627 'completion' => true,
628 'completionview' => true,
629 ]);
630
631 $cm = get_coursemodule_from_instance('choice', $choice->id);
632
633 // Let's manually create a course completion record instead of going thru the hoops to complete an activity.
634 if ($hasrecord) {
635 $cmcompletionrecord = (object)[
636 'coursemoduleid' => $cm->id,
637 'userid' => $user->id,
638 'completionstate' => $completion,
639 'viewed' => 0,
640 'overrideby' => null,
641 'timemodified' => 0,
642 ];
643 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
644 }
3871db0a 645
23603315
JP
646 // Whether we expect for the returned completion data to be stored in the cache.
647 $iscached = true;
4059b645 648
23603315
JP
649 if (!$sameuser) {
650 $iscached = false;
651 $this->setAdminUser();
652 } else {
653 $this->setUser($user);
654 }
4059b645 655
23603315
JP
656 // Mock other completion data.
657 $completioninfo = new completion_info($this->course);
658
659 $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
660 // Course module ID of the returned completion data must match this activity's course module ID.
661 $this->assertEquals($cm->id, $result->coursemoduleid);
662 // User ID of the returned completion data must match the user's ID.
663 $this->assertEquals($user->id, $result->userid);
664 // The completion state of the returned completion data must match the expected completion state.
665 $this->assertEquals($completion, $result->completionstate);
666
667 // If the user has no completion record, then the default record should be returned.
668 if (!$hasrecord) {
669 $iscached = false;
670 $this->assertEquals(0, $result->id);
671 }
672
673 // Check caching.
674 $key = "{$user->id}_{$this->course->id}";
675 $cache = cache::make('core', 'completion');
676 if ($iscached) {
677 // If we expect this to be cached, then fetching the result must match the cached data.
678 $this->assertEquals($result, (object)$cache->get($key)[$cm->id]);
679
680 // Check cached data for other course modules in the course.
681 // The sample module created in setup_data() should suffice to confirm this.
682 if ($wholecourse) {
683 $this->assertArrayHasKey($this->module1->id, $cache->get($key));
684 } else {
685 $this->assertArrayNotHasKey($this->module1->id, $cache->get($key));
686 }
687 } else {
688 // Otherwise, this should not be cached.
689 $this->assertFalse($cache->get($key));
690 }
4059b645
PS
691 }
692
82ff8e29 693 public function test_internal_set_data() {
3871db0a 694 global $DB;
cdc54199 695 $this->setup_data();
4059b645 696
cdc54199
RT
697 $this->setUser($this->user);
698 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
699 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
700 $cm = get_coursemodule_from_instance('forum', $forum->id);
701 $c = new completion_info($this->course);
4059b645 702
82ff8e29 703 // 1) Test with new data.
cdc54199
RT
704 $data = new stdClass();
705 $data->id = 0;
706 $data->userid = $this->user->id;
707 $data->coursemoduleid = $cm->id;
708 $data->completionstate = COMPLETION_COMPLETE;
709 $data->timemodified = time();
cd5be9a5 710 $data->viewed = COMPLETION_NOT_VIEWED;
60a6b36c 711 $data->overrideby = null;
4059b645
PS
712
713 $c->internal_set_data($cm, $data);
cdc54199
RT
714 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
715 $this->assertEquals($d1, $data->id);
3871db0a 716 $cache = cache::make('core', 'completion');
0cc9d709
MG
717 // Cache was not set for another user.
718 $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
719 $cache->get($data->userid . '_' . $cm->course));
4059b645 720
3871db0a 721 // 2) Test with existing data and for different user.
cdc54199
RT
722 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
723 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
724 $newuser = $this->getDataGenerator()->create_user();
725
726 $d2 = new stdClass();
727 $d2->id = 7;
728 $d2->userid = $newuser->id;
729 $d2->coursemoduleid = $cm2->id;
730 $d2->completionstate = COMPLETION_COMPLETE;
731 $d2->timemodified = time();
cd5be9a5 732 $d2->viewed = COMPLETION_NOT_VIEWED;
60a6b36c 733 $d2->overrideby = null;
cdc54199 734 $c->internal_set_data($cm2, $d2);
0cc9d709
MG
735 // Cache for current user returns the data.
736 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
737 $this->assertEquals($data, $cachevalue[$cm->id]);
738 // Cache for another user is not filled.
739 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
4059b645
PS
740
741 // 3) Test where it THINKS the data is new (from cache) but actually
82ff8e29
PS
742 // in the database it has been set since.
743 // 1) Test with new data.
cdc54199
RT
744 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
745 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
746 $newuser2 = $this->getDataGenerator()->create_user();
747 $d3 = new stdClass();
748 $d3->id = 13;
749 $d3->userid = $newuser2->id;
750 $d3->coursemoduleid = $cm3->id;
751 $d3->completionstate = COMPLETION_COMPLETE;
752 $d3->timemodified = time();
cd5be9a5 753 $d3->viewed = COMPLETION_NOT_VIEWED;
60a6b36c 754 $d3->overrideby = null;
cdc54199 755 $DB->insert_record('course_modules_completion', $d3);
4059b645
PS
756 $c->internal_set_data($cm, $data);
757 }
758
82ff8e29 759 public function test_get_progress_all() {
4059b645 760 global $DB;
82ff8e29 761 $this->mock_setup();
4059b645 762
52f3e060
RT
763 $mockbuilder = $this->getMockBuilder('completion_info');
764 $mockbuilder->setMethods(array('get_tracked_users'));
765 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
766 $c = $mockbuilder->getMock();
4059b645 767
82ff8e29 768 // 1) Basic usage.
4059b645
PS
769 $c->expects($this->at(0))
770 ->method('get_tracked_users')
771 ->with(false, array(), 0, '', '', '', null)
772 ->will($this->returnValue(array(
773 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
774 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
775 $DB->expects($this->at(0))
776 ->method('get_in_or_equal')
777 ->with(array(100, 201))
778 ->will($this->returnValue(array(' IN (100, 201)', array())));
779 $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
780 $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
781 $DB->expects($this->at(1))
782 ->method('get_recordset_sql')
82ff8e29 783 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
4059b645
PS
784
785 $this->assertEquals(array(
786 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
787 'progress'=>array(13=>$progress1)),
788 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
789 'progress'=>array(14=>$progress2)),
790 ), $c->get_progress_all(false));
791
82ff8e29 792 // 2) With more than 1, 000 results.
4059b645
PS
793 $tracked = array();
794 $ids = array();
795 $progress = array();
82ff8e29 796 for ($i = 100; $i<2000; $i++) {
4059b645
PS
797 $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
798 $ids[] = $i;
799 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
800 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
801 }
802 $c->expects($this->at(0))
803 ->method('get_tracked_users')
804 ->with(true, 3, 0, '', '', '', null)
805 ->will($this->returnValue($tracked));
806 $DB->expects($this->at(0))
807 ->method('get_in_or_equal')
808 ->with(array_slice($ids, 0, 1000))
809 ->will($this->returnValue(array(' IN whatever', array())));
810 $DB->expects($this->at(1))
811 ->method('get_recordset_sql')
82ff8e29 812 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
4059b645
PS
813
814 $DB->expects($this->at(2))
815 ->method('get_in_or_equal')
816 ->with(array_slice($ids, 1000))
817 ->will($this->returnValue(array(' IN whatever2', array())));
818 $DB->expects($this->at(3))
819 ->method('get_recordset_sql')
82ff8e29 820 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
4059b645
PS
821
822 $result = $c->get_progress_all(true, 3);
823 $resultok = true;
824 $resultok = $resultok && ($ids == array_keys($result));
825
82ff8e29 826 foreach ($result as $userid => $data) {
4059b645
PS
827 $resultok = $resultok && $data->firstname == 'frog';
828 $resultok = $resultok && $data->lastname == $userid;
829 $resultok = $resultok && $data->id == $userid;
830 $cms = $data->progress;
831 $resultok = $resultok && (array(13, 14) == array_keys($cms));
832 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
833 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
834 }
835 $this->assertTrue($resultok);
836 }
837
82ff8e29
PS
838 public function test_inform_grade_changed() {
839 $this->mock_setup();
840
52f3e060
RT
841 $mockbuilder = $this->getMockBuilder('completion_info');
842 $mockbuilder->setMethods(array('is_enabled', 'update_state'));
843 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
844 $c = $mockbuilder->getMock();
4059b645
PS
845
846 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
847 $item = (object)array('itemnumber'=>3, 'gradepass'=>1, 'hidden'=>0);
848 $grade = (object)array('userid'=>31337, 'finalgrade'=>0, 'rawgrade'=>0);
849
82ff8e29 850 // Not enabled (should do nothing).
4059b645
PS
851 $c->expects($this->at(0))
852 ->method('is_enabled')
853 ->with($cm)
854 ->will($this->returnValue(false));
855 $c->inform_grade_changed($cm, $item, $grade, false);
856
82ff8e29 857 // Enabled but still no grade completion required, should still do nothing.
4059b645
PS
858 $c->expects($this->at(0))
859 ->method('is_enabled')
860 ->with($cm)
861 ->will($this->returnValue(true));
862 $c->inform_grade_changed($cm, $item, $grade, false);
863
82ff8e29 864 // Enabled and completion required but item number is wrong, does nothing.
4059b645
PS
865 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
866 $c->expects($this->at(0))
867 ->method('is_enabled')
868 ->with($cm)
869 ->will($this->returnValue(true));
870 $c->inform_grade_changed($cm, $item, $grade, false);
871
872 // Enabled and completion required and item number right. It is supposed
873 // to call update_state with the new potential state being obtained from
874 // internal_get_grade_state.
875 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
876 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
877 $c->expects($this->at(0))
878 ->method('is_enabled')
879 ->with($cm)
880 ->will($this->returnValue(true));
881 $c->expects($this->at(1))
882 ->method('update_state')
883 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
884 ->will($this->returnValue(true));
885 $c->inform_grade_changed($cm, $item, $grade, false);
886
887 // Same as above but marked deleted. It is supposed to call update_state
82ff8e29 888 // with new potential state being COMPLETION_INCOMPLETE.
4059b645
PS
889 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
890 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
891 $c->expects($this->at(0))
892 ->method('is_enabled')
893 ->with($cm)
894 ->will($this->returnValue(true));
895 $c->expects($this->at(1))
896 ->method('update_state')
897 ->with($cm, COMPLETION_INCOMPLETE, 31337)
898 ->will($this->returnValue(true));
899 $c->inform_grade_changed($cm, $item, $grade, true);
900 }
901
82ff8e29
PS
902 public function test_internal_get_grade_state() {
903 $this->mock_setup();
904
4059b645
PS
905 $item = new stdClass;
906 $grade = new stdClass;
907
908 $item->gradepass = 4;
909 $item->hidden = 0;
910 $grade->rawgrade = 4.0;
911 $grade->finalgrade = null;
912
82ff8e29 913 // Grade has pass mark and is not hidden, user passes.
4059b645
PS
914 $this->assertEquals(
915 COMPLETION_COMPLETE_PASS,
916 completion_info::internal_get_grade_state($item, $grade));
917
82ff8e29 918 // Same but user fails.
4059b645
PS
919 $grade->rawgrade = 3.9;
920 $this->assertEquals(
921 COMPLETION_COMPLETE_FAIL,
922 completion_info::internal_get_grade_state($item, $grade));
923
82ff8e29 924 // User fails on raw grade but passes on final.
4059b645
PS
925 $grade->finalgrade = 4.0;
926 $this->assertEquals(
927 COMPLETION_COMPLETE_PASS,
928 completion_info::internal_get_grade_state($item, $grade));
929
82ff8e29 930 // Item is hidden.
4059b645
PS
931 $item->hidden = 1;
932 $this->assertEquals(
933 COMPLETION_COMPLETE,
934 completion_info::internal_get_grade_state($item, $grade));
935
82ff8e29 936 // Item isn't hidden but has no pass mark.
4059b645
PS
937 $item->hidden = 0;
938 $item->gradepass = 0;
939 $this->assertEquals(
940 COMPLETION_COMPLETE,
941 completion_info::internal_get_grade_state($item, $grade));
942 }
82ff8e29
PS
943
944 public function test_get_activities() {
7fbe33fc 945 global $CFG;
82ff8e29
PS
946 $this->resetAfterTest();
947
7fbe33fc
MG
948 // Enable completion before creating modules, otherwise the completion data is not written in DB.
949 $CFG->enablecompletion = true;
950
82ff8e29 951 // Create a course with mixed auto completion data.
7fbe33fc 952 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
953 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
954 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
955 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
956 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
957 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
958 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
959
960 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
961 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
962 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
963
964 // Create data in another course to make sure it's not considered.
7fbe33fc 965 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
966 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
967 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
968 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
969
970 $c = new completion_info($course);
971 $activities = $c->get_activities();
972 $this->assertCount(3, $activities);
973 $this->assertTrue(isset($activities[$forum->cmid]));
974 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
975 $this->assertTrue(isset($activities[$page->cmid]));
976 $this->assertSame($page->name, $activities[$page->cmid]->name);
977 $this->assertTrue(isset($activities[$data->cmid]));
978 $this->assertSame($data->name, $activities[$data->cmid]->name);
979
980 $this->assertFalse(isset($activities[$forum2->cmid]));
981 $this->assertFalse(isset($activities[$page2->cmid]));
982 $this->assertFalse(isset($activities[$data2->cmid]));
983 }
984
985 public function test_has_activities() {
7fbe33fc 986 global $CFG;
82ff8e29
PS
987 $this->resetAfterTest();
988
7fbe33fc
MG
989 // Enable completion before creating modules, otherwise the completion data is not written in DB.
990 $CFG->enablecompletion = true;
991
82ff8e29 992 // Create a course with mixed auto completion data.
7fbe33fc
MG
993 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
994 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
995 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
996 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
997 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
998 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
999
1000 $c1 = new completion_info($course);
1001 $c2 = new completion_info($course2);
1002
1003 $this->assertTrue($c1->has_activities());
1004 $this->assertFalse($c2->has_activities());
1005 }
cdc54199 1006
abde761c
PH
1007 /**
1008 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1009 *
1010 * @return void
1011 */
1012 public function test_course_delete_prerequisite() {
1013 global $DB;
1014
1015 $this->setup_data();
1016
1017 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1018
1019 $criteriadata = (object) [
1020 'id' => $this->course->id,
1021 'criteria_course' => [$courseprerequisite->id],
1022 ];
1023
1024 /** @var completion_criteria_course $criteria */
1025 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1026 $criteria->update_config($criteriadata);
1027
1028 // Sanity test.
1029 $this->assertTrue($DB->record_exists('course_completion_criteria', [
1030 'course' => $this->course->id,
1031 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1032 'courseinstance' => $courseprerequisite->id,
1033 ]));
1034
1035 // Deleting the prerequisite course should remove the completion criteria.
1036 delete_course($courseprerequisite, false);
1037
1038 $this->assertFalse($DB->record_exists('course_completion_criteria', [
1039 'course' => $this->course->id,
1040 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1041 'courseinstance' => $courseprerequisite->id,
1042 ]));
1043 }
1044
cdc54199
RT
1045 /**
1046 * Test course module completion update event.
1047 */
1048 public function test_course_module_completion_updated_event() {
7fbe33fc 1049 global $USER, $CFG;
cdc54199
RT
1050
1051 $this->setup_data();
7fbe33fc 1052
cdc54199
RT
1053 $this->setAdminUser();
1054
1055 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1056 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1057
1058 $c = new completion_info($this->course);
1059 $activities = $c->get_activities();
1060 $this->assertEquals(1, count($activities));
1061 $this->assertTrue(isset($activities[$forum->cmid]));
1062 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1063
1064 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1065 $current->completionstate = COMPLETION_COMPLETE;
1066 $current->timemodified = time();
1067 $sink = $this->redirectEvents();
1068 $c->internal_set_data($activities[$forum->cmid], $current);
1069 $events = $sink->get_events();
1070 $event = reset($events);
1071 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1072 $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1073 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
74b63eae 1074 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
cdc54199 1075 $this->assertEquals($USER->id, $event->userid);
02a5a4b2 1076 $this->assertEquals($this->user->id, $event->relateduserid);
fc4365d0 1077 $this->assertInstanceOf('moodle_url', $event->get_url());
cdc54199
RT
1078 $this->assertEventLegacyData($current, $event);
1079 }
1080
1cb1e5fe
RT
1081 /**
1082 * Test course completed event.
1083 */
1084 public function test_course_completed_event() {
1085 global $USER;
1086
1087 $this->setup_data();
1088 $this->setAdminUser();
1089
1090 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1091 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1092
1093 // Mark course as complete and get triggered event.
1094 $sink = $this->redirectEvents();
1095 $ccompletion->mark_complete();
1096 $events = $sink->get_events();
1097 $event = reset($events);
1098
1099 $this->assertInstanceOf('\core\event\course_completed', $event);
1100 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1101 $this->assertEquals($this->course->id, $event->courseid);
1102 $this->assertEquals($USER->id, $event->userid);
02a5a4b2 1103 $this->assertEquals($this->user->id, $event->relateduserid);
1cb1e5fe 1104 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
fc4365d0 1105 $this->assertInstanceOf('moodle_url', $event->get_url());
1cb1e5fe
RT
1106 $data = $ccompletion->get_record_data();
1107 $this->assertEventLegacyData($data, $event);
1108 }
4059b645 1109
417e5b9f
JL
1110 /**
1111 * Test course completed message.
1112 */
1113 public function test_course_completed_message() {
1114 $this->setup_data();
1115 $this->setAdminUser();
1116
1117 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1118 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1119
1120 // Mark course as complete and get the message.
1121 $sink = $this->redirectMessages();
1122 $ccompletion->mark_complete();
1123 $messages = $sink->get_messages();
1124 $sink->close();
1125
1126 $this->assertCount(1, $messages);
1127 $message = array_pop($messages);
1128
1129 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1130 $this->assertEquals($this->user->id, $message->useridto);
1131 $this->assertEquals('coursecompleted', $message->eventtype);
1132 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
40de097e 1133 $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
417e5b9f
JL
1134 }
1135
135dde74
RT
1136 /**
1137 * Test course completed event.
1138 */
1139 public function test_course_completion_updated_event() {
1140 $this->setup_data();
1141 $coursecontext = context_course::instance($this->course->id);
1142 $coursecompletionevent = \core\event\course_completion_updated::create(
1143 array(
1144 'courseid' => $this->course->id,
1145 'context' => $coursecontext
1146 )
1147 );
1148
1149 // Mark course as complete and get triggered event.
1150 $sink = $this->redirectEvents();
1151 $coursecompletionevent->trigger();
1152 $events = $sink->get_events();
1153 $event = array_pop($events);
1154 $sink->close();
1155
1156 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1157 $this->assertEquals($this->course->id, $event->courseid);
1158 $this->assertEquals($coursecontext, $event->get_context());
fc4365d0 1159 $this->assertInstanceOf('moodle_url', $event->get_url());
135dde74
RT
1160 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1161 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1162 }
194a02e4
DP
1163
1164 public function test_completion_can_view_data() {
1165 $this->setup_data();
1166
1167 $student = $this->getDataGenerator()->create_user();
1168 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1169
1170 $this->setUser($student);
1171 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1172 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1173 }
18ef213d
JP
1174
1175 /**
1176 * Data provider for test_get_grade_completion().
1177 *
1178 * @return array[]
1179 */
1180 public function get_grade_completion_provider() {
1181 return [
1182 'Grade not required' => [false, false, null, moodle_exception::class, null],
1183 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1184 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1185 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1186 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1187 ];
1188 }
1189
1190 /**
1191 * Test for \completion_info::get_grade_completion().
1192 *
1193 * @dataProvider get_grade_completion_provider
1194 * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1195 * @param bool $hasgrade Whether to set grade for the user in this activity.
1196 * @param int|null $passinggrade Passing grade to set for the test activity.
1197 * @param string|null $expectedexception Expected exception.
1198 * @param int|null $expectedresult The expected completion status.
1199 */
1200 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade, ?string $expectedexception,
1201 ?int $expectedresult) {
1202 $this->setup_data();
1203
1204 /** @var \mod_assign_generator $assigngenerator */
1205 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1206 $assign = $assigngenerator->create_instance([
1207 'course' => $this->course->id,
1208 'completion' => COMPLETION_ENABLED,
1209 'completionusegrade' => $completionusegrade,
1210 'gradepass' => $passinggrade,
1211 ]);
1212
1213 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1214 if ($completionusegrade && $hasgrade) {
1215 $assigninstance = new assign($cm->context, $cm, $this->course);
1216 $grade = $assigninstance->get_user_grade($this->user->id, true);
1217 $grade->grade = 75;
1218 $assigninstance->update_grade($grade);
1219 }
1220
1221 $completioninfo = new completion_info($this->course);
1222 if ($expectedexception) {
1223 $this->expectException($expectedexception);
1224 }
1225 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1226 $this->assertEquals($expectedresult, $gradecompletion);
1227 }
135dde74 1228}
4059b645 1229
82ff8e29
PS
1230class core_completionlib_fake_recordset implements Iterator {
1231 protected $closed;
1232 protected $values, $index;
4059b645 1233
82ff8e29 1234 public function __construct($values) {
4059b645
PS
1235 $this->values = $values;
1236 $this->index = 0;
1237 }
1238
82ff8e29 1239 public function current() {
4059b645
PS
1240 return $this->values[$this->index];
1241 }
1242
82ff8e29 1243 public function key() {
4059b645
PS
1244 return $this->values[$this->index];
1245 }
1246
82ff8e29 1247 public function next() {
4059b645
PS
1248 $this->index++;
1249 }
1250
82ff8e29 1251 public function rewind() {
4059b645
PS
1252 $this->index = 0;
1253 }
1254
82ff8e29 1255 public function valid() {
4059b645
PS
1256 return count($this->values) > $this->index;
1257 }
1258
82ff8e29 1259 public function close() {
4059b645
PS
1260 $this->closed = true;
1261 }
1262
82ff8e29 1263 public function was_closed() {
4059b645
PS
1264 return $this->closed;
1265 }
1266}