MDL-63303 message: add functions to message_repository.js
[moodle.git] / lib / tests / completionlib_test.php
blobcc2dfbb57c256a3747497c0b692e59b58db334a5
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Completion tests.
20 * @package core_completion
21 * @category phpunit
22 * @copyright 2008 Sam Marshall
23 * @copyright 2013 Frédéric Massart
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir.'/completionlib.php');
32 class core_completionlib_testcase extends advanced_testcase {
33 protected $course;
34 protected $user;
35 protected $module1;
36 protected $module2;
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);
48 /**
49 * Create course with user and activities.
51 protected function setup_data() {
52 global $DB, $CFG;
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));
68 /**
69 * Asserts that two variables are equal.
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
79 public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
80 // Nasty cheating hack: prevent random failures on timemodified field.
81 if (is_object($expected) and is_object($actual)) {
82 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
83 if ($expected->timemodified + 1 == $actual->timemodified) {
84 $expected = clone($expected);
85 $expected->timemodified = $actual->timemodified;
89 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
92 public function test_is_enabled() {
93 global $CFG;
94 $this->mock_setup();
96 // Config alone.
97 $CFG->enablecompletion = COMPLETION_DISABLED;
98 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
99 $CFG->enablecompletion = COMPLETION_ENABLED;
100 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
102 // Course.
103 $course = (object)array('id' =>13);
104 $c = new completion_info($course);
105 $course->enablecompletion = COMPLETION_DISABLED;
106 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
107 $course->enablecompletion = COMPLETION_ENABLED;
108 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
109 $CFG->enablecompletion = COMPLETION_DISABLED;
110 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
112 // Course and CM.
113 $cm = new stdClass();
114 $cm->completion = COMPLETION_TRACKING_MANUAL;
115 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
116 $CFG->enablecompletion = COMPLETION_ENABLED;
117 $course->enablecompletion = COMPLETION_DISABLED;
118 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
119 $course->enablecompletion = COMPLETION_ENABLED;
120 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
121 $cm->completion = COMPLETION_TRACKING_NONE;
122 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
123 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
124 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
127 public function test_update_state() {
128 $this->mock_setup();
130 $mockbuilder = $this->getMockBuilder('completion_info');
131 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
132 'user_can_override_completion'));
133 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
134 $c = $mockbuilder->getMock();
135 $cm = (object)array('id'=>13, 'course'=>42);
137 // Not enabled, should do nothing.
138 $c->expects($this->at(0))
139 ->method('is_enabled')
140 ->with($cm)
141 ->will($this->returnValue(false));
142 $c->update_state($cm);
144 // Enabled, but current state is same as possible result, do nothing.
145 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
146 $c->expects($this->at(0))
147 ->method('is_enabled')
148 ->with($cm)
149 ->will($this->returnValue(true));
150 $c->expects($this->at(1))
151 ->method('get_data')
152 ->with($cm, false, 0)
153 ->will($this->returnValue($current));
154 $c->update_state($cm, COMPLETION_COMPLETE);
156 // Enabled, but current state is a specific one and new state is just
157 // complete, so do nothing.
158 $current->completionstate = COMPLETION_COMPLETE_PASS;
159 $c->expects($this->at(0))
160 ->method('is_enabled')
161 ->with($cm)
162 ->will($this->returnValue(true));
163 $c->expects($this->at(1))
164 ->method('get_data')
165 ->with($cm, false, 0)
166 ->will($this->returnValue($current));
167 $c->update_state($cm, COMPLETION_COMPLETE);
169 // Manual, change state (no change).
170 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
171 $current->completionstate=COMPLETION_COMPLETE;
172 $c->expects($this->at(0))
173 ->method('is_enabled')
174 ->with($cm)
175 ->will($this->returnValue(true));
176 $c->expects($this->at(1))
177 ->method('get_data')
178 ->with($cm, false, 0)
179 ->will($this->returnValue($current));
180 $c->update_state($cm, COMPLETION_COMPLETE);
182 // Manual, change state (change).
183 $c->expects($this->at(0))
184 ->method('is_enabled')
185 ->with($cm)
186 ->will($this->returnValue(true));
187 $c->expects($this->at(1))
188 ->method('get_data')
189 ->with($cm, false, 0)
190 ->will($this->returnValue($current));
191 $changed = clone($current);
192 $changed->timemodified = time();
193 $changed->completionstate = COMPLETION_INCOMPLETE;
194 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
195 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
196 $c->expects($this->at(2))
197 ->method('internal_set_data')
198 ->with($cm, $comparewith);
199 $c->update_state($cm, COMPLETION_INCOMPLETE);
201 // Auto, change state.
202 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
203 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
204 $c->expects($this->at(0))
205 ->method('is_enabled')
206 ->with($cm)
207 ->will($this->returnValue(true));
208 $c->expects($this->at(1))
209 ->method('get_data')
210 ->with($cm, false, 0)
211 ->will($this->returnValue($current));
212 $c->expects($this->at(2))
213 ->method('internal_get_state')
214 ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
215 $changed = clone($current);
216 $changed->timemodified = time();
217 $changed->completionstate = COMPLETION_COMPLETE_PASS;
218 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
219 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
220 $c->expects($this->at(3))
221 ->method('internal_set_data')
222 ->with($cm, $comparewith);
223 $c->update_state($cm, COMPLETION_COMPLETE_PASS);
225 // Manual tracking, change state by overriding it manually.
226 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
227 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
228 $c->expects($this->at(0))
229 ->method('is_enabled')
230 ->with($cm)
231 ->will($this->returnValue(true));
232 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
233 ->method('user_can_override_completion')
234 ->will($this->returnValue(true));
235 $c->expects($this->at(2))
236 ->method('get_data')
237 ->with($cm, false, 100)
238 ->will($this->returnValue($current));
239 $changed = clone($current);
240 $changed->timemodified = time();
241 $changed->completionstate = COMPLETION_COMPLETE;
242 $changed->overrideby = 314159;
243 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
244 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
245 $c->expects($this->at(3))
246 ->method('internal_set_data')
247 ->with($cm, $comparewith);
248 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
249 // And confirm that the status can be changed back to incomplete without an override.
250 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
251 $c->expects($this->at(0))
252 ->method('get_data')
253 ->with($cm, false, 100)
254 ->will($this->returnValue($current));
255 $c->get_data($cm, false, 100);
257 // Auto, change state via override, incomplete to complete.
258 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
259 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
260 $c->expects($this->at(0))
261 ->method('is_enabled')
262 ->with($cm)
263 ->will($this->returnValue(true));
264 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
265 ->method('user_can_override_completion')
266 ->will($this->returnValue(true));
267 $c->expects($this->at(2))
268 ->method('get_data')
269 ->with($cm, false, 100)
270 ->will($this->returnValue($current));
271 $changed = clone($current);
272 $changed->timemodified = time();
273 $changed->completionstate = COMPLETION_COMPLETE;
274 $changed->overrideby = 314159;
275 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
276 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
277 $c->expects($this->at(3))
278 ->method('internal_set_data')
279 ->with($cm, $comparewith);
280 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
281 $c->expects($this->at(0))
282 ->method('get_data')
283 ->with($cm, false, 100)
284 ->will($this->returnValue($changed));
285 $c->get_data($cm, false, 100);
287 // Now confirm that the status cannot be changed back to incomplete without an override.
288 // I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden.
289 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
290 $c->expects($this->at(0))
291 ->method('get_data')
292 ->with($cm, false, 100)
293 ->will($this->returnValue($changed));
294 $c->get_data($cm, false, 100);
296 // Now confirm the status can be changed back from complete to incomplete using an override.
297 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
298 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
299 $c->expects($this->at(0))
300 ->method('is_enabled')
301 ->with($cm)
302 ->will($this->returnValue(true));
303 $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
304 ->method('user_can_override_completion')
305 ->will($this->returnValue(true));
306 $c->expects($this->at(2))
307 ->method('get_data')
308 ->with($cm, false, 100)
309 ->will($this->returnValue($current));
310 $changed = clone($current);
311 $changed->timemodified = time();
312 $changed->completionstate = COMPLETION_INCOMPLETE;
313 $changed->overrideby = 314159;
314 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
315 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
316 $c->expects($this->at(3))
317 ->method('internal_set_data')
318 ->with($cm, $comparewith);
319 $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
320 $c->expects($this->at(0))
321 ->method('get_data')
322 ->with($cm, false, 100)
323 ->will($this->returnValue($changed));
324 $c->get_data($cm, false, 100);
327 public function test_internal_get_state() {
328 global $DB;
329 $this->mock_setup();
331 $mockbuilder = $this->getMockBuilder('completion_info');
332 $mockbuilder->setMethods(array('internal_get_grade_state'));
333 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
334 $c = $mockbuilder->getMock();
336 $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
338 // If view is required, but they haven't viewed it yet.
339 $cm->completionview = COMPLETION_VIEW_REQUIRED;
340 $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
341 $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
343 // OK set view not required.
344 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
346 // Test not getting module name.
347 $cm->modname='label';
348 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
350 // Test getting module name.
351 $cm->module = 13;
352 unset($cm->modname);
353 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
354 $DB->expects($this->once())
355 ->method('get_field')
356 ->with('modules', 'name', array('id'=>13))
357 ->will($this->returnValue('lable'));
358 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
360 // Note: This function is not fully tested (including kind of the main part) because:
361 // * the grade_item/grade_grade calls are static and can't be mocked,
362 // * the plugin_supports call is static and can't be mocked.
365 public function test_set_module_viewed() {
366 $this->mock_setup();
368 $mockbuilder = $this->getMockBuilder('completion_info');
369 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
370 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
371 $c = $mockbuilder->getMock();
372 $cm = (object)array('id'=>13, 'course'=>42);
374 // Not tracking completion, should do nothing.
375 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
376 $c->set_module_viewed($cm);
378 // Tracking completion but completion is disabled, should do nothing.
379 $cm->completionview = COMPLETION_VIEW_REQUIRED;
380 $c->expects($this->at(0))
381 ->method('is_enabled')
382 ->with($cm)
383 ->will($this->returnValue(false));
384 $c->set_module_viewed($cm);
386 // Now it's enabled, we expect it to get data. If data already has
387 // viewed, still do nothing.
388 $c->expects($this->at(0))
389 ->method('is_enabled')
390 ->with($cm)
391 ->will($this->returnValue(true));
392 $c->expects($this->at(1))
393 ->method('get_data')
394 ->with($cm, 0)
395 ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
396 $c->set_module_viewed($cm);
398 // OK finally one that hasn't been viewed, now it should set it viewed
399 // and update state.
400 $c->expects($this->at(0))
401 ->method('is_enabled')
402 ->with($cm)
403 ->will($this->returnValue(true));
404 $c->expects($this->at(1))
405 ->method('get_data')
406 ->with($cm, false, 1337)
407 ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
408 $c->expects($this->at(2))
409 ->method('internal_set_data')
410 ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
411 $c->expects($this->at(3))
412 ->method('update_state')
413 ->with($cm, COMPLETION_COMPLETE, 1337);
414 $c->set_module_viewed($cm, 1337);
417 public function test_count_user_data() {
418 global $DB;
419 $this->mock_setup();
421 $course = (object)array('id'=>13);
422 $cm = (object)array('id'=>42);
424 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
425 $DB->expects($this->at(0))
426 ->method('get_field_sql')
427 ->will($this->returnValue(666));
429 $c = new completion_info($course);
430 $this->assertEquals(666, $c->count_user_data($cm));
433 public function test_delete_all_state() {
434 global $DB;
435 $this->mock_setup();
437 $course = (object)array('id'=>13);
438 $cm = (object)array('id'=>42, 'course'=>13);
439 $c = new completion_info($course);
441 // Check it works ok without data in session.
442 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
443 $DB->expects($this->at(0))
444 ->method('delete_records')
445 ->with('course_modules_completion', array('coursemoduleid'=>42))
446 ->will($this->returnValue(true));
447 $c->delete_all_state($cm);
450 public function test_reset_all_state() {
451 global $DB;
452 $this->mock_setup();
454 $mockbuilder = $this->getMockBuilder('completion_info');
455 $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
456 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
457 $c = $mockbuilder->getMock();
459 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
461 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
462 $DB->expects($this->at(0))
463 ->method('get_recordset')
464 ->will($this->returnValue(
465 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
467 $c->expects($this->at(0))
468 ->method('delete_all_state')
469 ->with($cm);
471 $c->expects($this->at(1))
472 ->method('get_tracked_users')
473 ->will($this->returnValue(array(
474 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
475 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
477 $c->expects($this->at(2))
478 ->method('update_state')
479 ->with($cm, COMPLETION_UNKNOWN, 100);
480 $c->expects($this->at(3))
481 ->method('update_state')
482 ->with($cm, COMPLETION_UNKNOWN, 101);
483 $c->expects($this->at(4))
484 ->method('update_state')
485 ->with($cm, COMPLETION_UNKNOWN, 201);
487 $c->reset_all_state($cm);
490 public function test_get_data() {
491 global $DB;
492 $this->mock_setup();
494 $cache = cache::make('core', 'completion');
496 $c = new completion_info((object)array('id'=>42, 'cacherev'=>1));
497 $cm = (object)array('id'=>13, 'course'=>42);
499 // 1. Not current user, record exists.
500 $sillyrecord = (object)array('frog'=>'kermit');
502 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
503 $DB->expects($this->at(0))
504 ->method('get_record')
505 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>123))
506 ->will($this->returnValue($sillyrecord));
507 $result = $c->get_data($cm, false, 123);
508 $this->assertEquals($sillyrecord, $result);
509 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
511 // 2. Not current user, default record, whole course.
512 $cache->purge();
513 $DB->expects($this->at(0))
514 ->method('get_records_sql')
515 ->will($this->returnValue(array()));
516 $modinfo = new stdClass();
517 $modinfo->cms = array((object)array('id'=>13));
518 $result=$c->get_data($cm, true, 123, $modinfo);
519 $this->assertEquals((object)array(
520 'id' => '0', 'coursemoduleid' => 13, 'userid' => 123, 'completionstate' => 0,
521 'viewed' => 0, 'timemodified' => 0, 'overrideby' => 0), $result);
522 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
524 // 3. Current user, single record, not from cache.
525 $DB->expects($this->at(0))
526 ->method('get_record')
527 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>314159))
528 ->will($this->returnValue($sillyrecord));
529 $result = $c->get_data($cm);
530 $this->assertEquals($sillyrecord, $result);
531 $cachevalue = $cache->get('314159_42');
532 $this->assertEquals((array)$sillyrecord, $cachevalue[13]);
534 // 4. Current user, 'whole course', but from cache.
535 $result = $c->get_data($cm, true);
536 $this->assertEquals($sillyrecord, $result);
538 // 5. Current user, 'whole course' and record not in cache.
539 $cache->purge();
541 // Scenario: Completion data exists for one CMid.
542 $basicrecord = (object)array('coursemoduleid'=>13);
543 $DB->expects($this->at(0))
544 ->method('get_records_sql')
545 ->will($this->returnValue(array('1'=>$basicrecord)));
547 // There are two CMids in total, the one we had data for and another one.
548 $modinfo = new stdClass();
549 $modinfo->cms = array((object)array('id'=>13), (object)array('id'=>14));
550 $result = $c->get_data($cm, true, 0, $modinfo);
552 // Check result.
553 $this->assertEquals($basicrecord, $result);
555 // Check the cache contents.
556 $cachevalue = $cache->get('314159_42');
557 $this->assertEquals($basicrecord, (object)$cachevalue[13]);
558 $this->assertEquals(array('id' => '0', 'coursemoduleid' => 14,
559 'userid' => 314159, 'completionstate' => 0, 'viewed' => 0, 'overrideby' => 0, 'timemodified' => 0),
560 $cachevalue[14]);
563 public function test_internal_set_data() {
564 global $DB;
565 $this->setup_data();
567 $this->setUser($this->user);
568 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
569 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
570 $cm = get_coursemodule_from_instance('forum', $forum->id);
571 $c = new completion_info($this->course);
573 // 1) Test with new data.
574 $data = new stdClass();
575 $data->id = 0;
576 $data->userid = $this->user->id;
577 $data->coursemoduleid = $cm->id;
578 $data->completionstate = COMPLETION_COMPLETE;
579 $data->timemodified = time();
580 $data->viewed = COMPLETION_NOT_VIEWED;
581 $data->overrideby = null;
583 $c->internal_set_data($cm, $data);
584 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
585 $this->assertEquals($d1, $data->id);
586 $cache = cache::make('core', 'completion');
587 // Cache was not set for another user.
588 $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
589 $cache->get($data->userid . '_' . $cm->course));
591 // 2) Test with existing data and for different user.
592 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
593 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
594 $newuser = $this->getDataGenerator()->create_user();
596 $d2 = new stdClass();
597 $d2->id = 7;
598 $d2->userid = $newuser->id;
599 $d2->coursemoduleid = $cm2->id;
600 $d2->completionstate = COMPLETION_COMPLETE;
601 $d2->timemodified = time();
602 $d2->viewed = COMPLETION_NOT_VIEWED;
603 $d2->overrideby = null;
604 $c->internal_set_data($cm2, $d2);
605 // Cache for current user returns the data.
606 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
607 $this->assertEquals($data, $cachevalue[$cm->id]);
608 // Cache for another user is not filled.
609 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
611 // 3) Test where it THINKS the data is new (from cache) but actually
612 // in the database it has been set since.
613 // 1) Test with new data.
614 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
615 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
616 $newuser2 = $this->getDataGenerator()->create_user();
617 $d3 = new stdClass();
618 $d3->id = 13;
619 $d3->userid = $newuser2->id;
620 $d3->coursemoduleid = $cm3->id;
621 $d3->completionstate = COMPLETION_COMPLETE;
622 $d3->timemodified = time();
623 $d3->viewed = COMPLETION_NOT_VIEWED;
624 $d3->overrideby = null;
625 $DB->insert_record('course_modules_completion', $d3);
626 $c->internal_set_data($cm, $data);
629 public function test_get_progress_all() {
630 global $DB;
631 $this->mock_setup();
633 $mockbuilder = $this->getMockBuilder('completion_info');
634 $mockbuilder->setMethods(array('get_tracked_users'));
635 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
636 $c = $mockbuilder->getMock();
638 // 1) Basic usage.
639 $c->expects($this->at(0))
640 ->method('get_tracked_users')
641 ->with(false, array(), 0, '', '', '', null)
642 ->will($this->returnValue(array(
643 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
644 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
645 $DB->expects($this->at(0))
646 ->method('get_in_or_equal')
647 ->with(array(100, 201))
648 ->will($this->returnValue(array(' IN (100, 201)', array())));
649 $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
650 $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
651 $DB->expects($this->at(1))
652 ->method('get_recordset_sql')
653 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
655 $this->assertEquals(array(
656 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
657 'progress'=>array(13=>$progress1)),
658 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
659 'progress'=>array(14=>$progress2)),
660 ), $c->get_progress_all(false));
662 // 2) With more than 1, 000 results.
663 $tracked = array();
664 $ids = array();
665 $progress = array();
666 for ($i = 100; $i<2000; $i++) {
667 $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
668 $ids[] = $i;
669 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
670 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
672 $c->expects($this->at(0))
673 ->method('get_tracked_users')
674 ->with(true, 3, 0, '', '', '', null)
675 ->will($this->returnValue($tracked));
676 $DB->expects($this->at(0))
677 ->method('get_in_or_equal')
678 ->with(array_slice($ids, 0, 1000))
679 ->will($this->returnValue(array(' IN whatever', array())));
680 $DB->expects($this->at(1))
681 ->method('get_recordset_sql')
682 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
684 $DB->expects($this->at(2))
685 ->method('get_in_or_equal')
686 ->with(array_slice($ids, 1000))
687 ->will($this->returnValue(array(' IN whatever2', array())));
688 $DB->expects($this->at(3))
689 ->method('get_recordset_sql')
690 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
692 $result = $c->get_progress_all(true, 3);
693 $resultok = true;
694 $resultok = $resultok && ($ids == array_keys($result));
696 foreach ($result as $userid => $data) {
697 $resultok = $resultok && $data->firstname == 'frog';
698 $resultok = $resultok && $data->lastname == $userid;
699 $resultok = $resultok && $data->id == $userid;
700 $cms = $data->progress;
701 $resultok = $resultok && (array(13, 14) == array_keys($cms));
702 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
703 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
705 $this->assertTrue($resultok);
708 public function test_inform_grade_changed() {
709 $this->mock_setup();
711 $mockbuilder = $this->getMockBuilder('completion_info');
712 $mockbuilder->setMethods(array('is_enabled', 'update_state'));
713 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
714 $c = $mockbuilder->getMock();
716 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
717 $item = (object)array('itemnumber'=>3, 'gradepass'=>1, 'hidden'=>0);
718 $grade = (object)array('userid'=>31337, 'finalgrade'=>0, 'rawgrade'=>0);
720 // Not enabled (should do nothing).
721 $c->expects($this->at(0))
722 ->method('is_enabled')
723 ->with($cm)
724 ->will($this->returnValue(false));
725 $c->inform_grade_changed($cm, $item, $grade, false);
727 // Enabled but still no grade completion required, should still do nothing.
728 $c->expects($this->at(0))
729 ->method('is_enabled')
730 ->with($cm)
731 ->will($this->returnValue(true));
732 $c->inform_grade_changed($cm, $item, $grade, false);
734 // Enabled and completion required but item number is wrong, does nothing.
735 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
736 $c->expects($this->at(0))
737 ->method('is_enabled')
738 ->with($cm)
739 ->will($this->returnValue(true));
740 $c->inform_grade_changed($cm, $item, $grade, false);
742 // Enabled and completion required and item number right. It is supposed
743 // to call update_state with the new potential state being obtained from
744 // internal_get_grade_state.
745 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
746 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
747 $c->expects($this->at(0))
748 ->method('is_enabled')
749 ->with($cm)
750 ->will($this->returnValue(true));
751 $c->expects($this->at(1))
752 ->method('update_state')
753 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
754 ->will($this->returnValue(true));
755 $c->inform_grade_changed($cm, $item, $grade, false);
757 // Same as above but marked deleted. It is supposed to call update_state
758 // with new potential state being COMPLETION_INCOMPLETE.
759 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
760 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
761 $c->expects($this->at(0))
762 ->method('is_enabled')
763 ->with($cm)
764 ->will($this->returnValue(true));
765 $c->expects($this->at(1))
766 ->method('update_state')
767 ->with($cm, COMPLETION_INCOMPLETE, 31337)
768 ->will($this->returnValue(true));
769 $c->inform_grade_changed($cm, $item, $grade, true);
772 public function test_internal_get_grade_state() {
773 $this->mock_setup();
775 $item = new stdClass;
776 $grade = new stdClass;
778 $item->gradepass = 4;
779 $item->hidden = 0;
780 $grade->rawgrade = 4.0;
781 $grade->finalgrade = null;
783 // Grade has pass mark and is not hidden, user passes.
784 $this->assertEquals(
785 COMPLETION_COMPLETE_PASS,
786 completion_info::internal_get_grade_state($item, $grade));
788 // Same but user fails.
789 $grade->rawgrade = 3.9;
790 $this->assertEquals(
791 COMPLETION_COMPLETE_FAIL,
792 completion_info::internal_get_grade_state($item, $grade));
794 // User fails on raw grade but passes on final.
795 $grade->finalgrade = 4.0;
796 $this->assertEquals(
797 COMPLETION_COMPLETE_PASS,
798 completion_info::internal_get_grade_state($item, $grade));
800 // Item is hidden.
801 $item->hidden = 1;
802 $this->assertEquals(
803 COMPLETION_COMPLETE,
804 completion_info::internal_get_grade_state($item, $grade));
806 // Item isn't hidden but has no pass mark.
807 $item->hidden = 0;
808 $item->gradepass = 0;
809 $this->assertEquals(
810 COMPLETION_COMPLETE,
811 completion_info::internal_get_grade_state($item, $grade));
814 public function test_get_activities() {
815 global $CFG;
816 $this->resetAfterTest();
818 // Enable completion before creating modules, otherwise the completion data is not written in DB.
819 $CFG->enablecompletion = true;
821 // Create a course with mixed auto completion data.
822 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
823 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
824 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
825 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
826 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
827 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
828 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
830 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
831 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
832 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
834 // Create data in another course to make sure it's not considered.
835 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
836 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
837 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
838 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
840 $c = new completion_info($course);
841 $activities = $c->get_activities();
842 $this->assertCount(3, $activities);
843 $this->assertTrue(isset($activities[$forum->cmid]));
844 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
845 $this->assertTrue(isset($activities[$page->cmid]));
846 $this->assertSame($page->name, $activities[$page->cmid]->name);
847 $this->assertTrue(isset($activities[$data->cmid]));
848 $this->assertSame($data->name, $activities[$data->cmid]->name);
850 $this->assertFalse(isset($activities[$forum2->cmid]));
851 $this->assertFalse(isset($activities[$page2->cmid]));
852 $this->assertFalse(isset($activities[$data2->cmid]));
855 public function test_has_activities() {
856 global $CFG;
857 $this->resetAfterTest();
859 // Enable completion before creating modules, otherwise the completion data is not written in DB.
860 $CFG->enablecompletion = true;
862 // Create a course with mixed auto completion data.
863 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
864 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
865 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
866 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
867 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
868 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
870 $c1 = new completion_info($course);
871 $c2 = new completion_info($course2);
873 $this->assertTrue($c1->has_activities());
874 $this->assertFalse($c2->has_activities());
878 * Test course module completion update event.
880 public function test_course_module_completion_updated_event() {
881 global $USER, $CFG;
883 $this->setup_data();
885 $this->setAdminUser();
887 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
888 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
890 $c = new completion_info($this->course);
891 $activities = $c->get_activities();
892 $this->assertEquals(1, count($activities));
893 $this->assertTrue(isset($activities[$forum->cmid]));
894 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
896 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
897 $current->completionstate = COMPLETION_COMPLETE;
898 $current->timemodified = time();
899 $sink = $this->redirectEvents();
900 $c->internal_set_data($activities[$forum->cmid], $current);
901 $events = $sink->get_events();
902 $event = reset($events);
903 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
904 $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
905 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
906 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
907 $this->assertEquals($USER->id, $event->userid);
908 $this->assertEquals($this->user->id, $event->relateduserid);
909 $this->assertInstanceOf('moodle_url', $event->get_url());
910 $this->assertEventLegacyData($current, $event);
914 * Test course completed event.
916 public function test_course_completed_event() {
917 global $USER;
919 $this->setup_data();
920 $this->setAdminUser();
922 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
923 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
925 // Mark course as complete and get triggered event.
926 $sink = $this->redirectEvents();
927 $ccompletion->mark_complete();
928 $events = $sink->get_events();
929 $event = reset($events);
931 $this->assertInstanceOf('\core\event\course_completed', $event);
932 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
933 $this->assertEquals($this->course->id, $event->courseid);
934 $this->assertEquals($USER->id, $event->userid);
935 $this->assertEquals($this->user->id, $event->relateduserid);
936 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
937 $this->assertInstanceOf('moodle_url', $event->get_url());
938 $data = $ccompletion->get_record_data();
939 $this->assertEventLegacyData($data, $event);
943 * Test course completed event.
945 public function test_course_completion_updated_event() {
946 $this->setup_data();
947 $coursecontext = context_course::instance($this->course->id);
948 $coursecompletionevent = \core\event\course_completion_updated::create(
949 array(
950 'courseid' => $this->course->id,
951 'context' => $coursecontext
955 // Mark course as complete and get triggered event.
956 $sink = $this->redirectEvents();
957 $coursecompletionevent->trigger();
958 $events = $sink->get_events();
959 $event = array_pop($events);
960 $sink->close();
962 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
963 $this->assertEquals($this->course->id, $event->courseid);
964 $this->assertEquals($coursecontext, $event->get_context());
965 $this->assertInstanceOf('moodle_url', $event->get_url());
966 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
967 $this->assertEventLegacyLogData($expectedlegacylog, $event);
970 public function test_completion_can_view_data() {
971 $this->setup_data();
973 $student = $this->getDataGenerator()->create_user();
974 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
976 $this->setUser($student);
977 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
978 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
982 class core_completionlib_fake_recordset implements Iterator {
983 protected $closed;
984 protected $values, $index;
986 public function __construct($values) {
987 $this->values = $values;
988 $this->index = 0;
991 public function current() {
992 return $this->values[$this->index];
995 public function key() {
996 return $this->values[$this->index];
999 public function next() {
1000 $this->index++;
1003 public function rewind() {
1004 $this->index = 0;
1007 public function valid() {
1008 return count($this->values) > $this->index;
1011 public function close() {
1012 $this->closed = true;
1015 public function was_closed() {
1016 return $this->closed;