Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
defined('MOODLE_INTERNAL') || die();
18
 
19
global $CFG;
20
require_once($CFG->libdir.'/completionlib.php');
21
 
22
/**
23
 * Completion tests.
24
 *
25
 * @package    core_completion
26
 * @category   test
27
 * @copyright  2008 Sam Marshall
28
 * @copyright  2013 Frédéric Massart
29
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 * @coversDefaultClass \completion_info
31
 */
32
class completionlib_test extends advanced_testcase {
33
    protected $course;
34
    protected $user;
35
    protected $module1;
36
    protected $module2;
37
 
38
    protected function mock_setup() {
39
        global $DB, $CFG, $USER;
40
 
41
        $this->resetAfterTest();
42
 
43
        $DB = $this->createMock(get_class($DB));
44
        $CFG->enablecompletion = COMPLETION_ENABLED;
45
        $USER = (object)array('id' => 314159);
46
    }
47
 
48
    /**
49
     * Create course with user and activities.
50
     */
51
    protected function setup_data() {
52
        global $DB, $CFG;
53
 
54
        $this->resetAfterTest();
55
 
56
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
57
        $CFG->enablecompletion = true;
58
 
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);
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
 
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
     */
79
    public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
80
                                        bool $canonicalize = false, bool $ignoreCase = false): void {
81
        // Nasty cheating hack: prevent random failures on timemodified field.
82
        if (is_array($actual) && (is_object($expected) || is_array($expected))) {
83
            $actual = (object) $actual;
84
            $expected = (object) $expected;
85
        }
86
        if (is_object($expected) and is_object($actual)) {
87
            if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
88
                if ($expected->timemodified + 1 == $actual->timemodified) {
89
                    $expected = clone($expected);
90
                    $expected->timemodified = $actual->timemodified;
91
                }
92
            }
93
        }
94
        parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
95
    }
96
 
97
    /**
98
     * @covers ::is_enabled_for_site
99
     * @covers ::is_enabled
100
     */
11 efrain 101
    public function test_is_enabled(): void {
1 efrain 102
        global $CFG;
103
        $this->mock_setup();
104
 
105
        // Config alone.
106
        $CFG->enablecompletion = COMPLETION_DISABLED;
107
        $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
108
        $CFG->enablecompletion = COMPLETION_ENABLED;
109
        $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
110
 
111
        // Course.
112
        $course = (object)array('id' => 13);
113
        $c = new completion_info($course);
114
        $course->enablecompletion = COMPLETION_DISABLED;
115
        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
116
        $course->enablecompletion = COMPLETION_ENABLED;
117
        $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
118
        $CFG->enablecompletion = COMPLETION_DISABLED;
119
        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
120
 
121
        // Course and CM.
122
        $cm = new stdClass();
123
        $cm->completion = COMPLETION_TRACKING_MANUAL;
124
        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
125
        $CFG->enablecompletion = COMPLETION_ENABLED;
126
        $course->enablecompletion = COMPLETION_DISABLED;
127
        $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
128
        $course->enablecompletion = COMPLETION_ENABLED;
129
        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
130
        $cm->completion = COMPLETION_TRACKING_NONE;
131
        $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
132
        $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
133
        $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
134
    }
135
 
136
    /**
137
     * @covers ::update_state
138
     */
11 efrain 139
    public function test_update_state(): void {
1 efrain 140
        $this->mock_setup();
141
 
142
        $mockbuilder = $this->getMockBuilder('completion_info');
143
        $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
144
                                       'user_can_override_completion'));
145
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
146
        $cm = (object)array('id' => 13, 'course' => 42);
147
 
148
        // Not enabled, should do nothing.
149
        $c = $mockbuilder->getMock();
150
        $c->expects($this->once())
151
            ->method('is_enabled')
152
            ->with($cm)
153
            ->will($this->returnValue(false));
154
        $c->update_state($cm);
155
 
156
        // Enabled, but current state is same as possible result, do nothing.
157
        $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
158
        $c = $mockbuilder->getMock();
159
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
160
        $c->expects($this->once())
161
            ->method('is_enabled')
162
            ->with($cm)
163
            ->will($this->returnValue(true));
164
        $c->expects($this->once())
165
            ->method('get_data')
166
            ->will($this->returnValue($current));
167
        $c->update_state($cm, COMPLETION_COMPLETE);
168
 
169
        // Enabled, but current state is a specific one and new state is just
170
        // complete, so do nothing.
171
        $c = $mockbuilder->getMock();
172
        $current->completionstate = COMPLETION_COMPLETE_PASS;
173
        $c->expects($this->once())
174
            ->method('is_enabled')
175
            ->with($cm)
176
            ->will($this->returnValue(true));
177
        $c->expects($this->once())
178
            ->method('get_data')
179
            ->will($this->returnValue($current));
180
        $c->update_state($cm, COMPLETION_COMPLETE);
181
 
182
        // Manual, change state (no change).
183
        $c = $mockbuilder->getMock();
184
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
185
        $current->completionstate = COMPLETION_COMPLETE;
186
        $c->expects($this->once())
187
            ->method('is_enabled')
188
            ->with($cm)
189
            ->will($this->returnValue(true));
190
        $c->expects($this->once())
191
            ->method('get_data')
192
            ->will($this->returnValue($current));
193
        $c->update_state($cm, COMPLETION_COMPLETE);
194
 
195
        // Manual, change state (change).
196
        $c = $mockbuilder->getMock();
197
        $c->expects($this->once())
198
            ->method('is_enabled')
199
            ->with($cm)
200
            ->will($this->returnValue(true));
201
        $c->expects($this->once())
202
            ->method('get_data')
203
            ->will($this->returnValue($current));
204
        $changed = clone($current);
205
        $changed->timemodified = time();
206
        $changed->completionstate = COMPLETION_INCOMPLETE;
207
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
208
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
209
        $c->expects($this->once())
210
            ->method('internal_set_data')
211
            ->with($cm, $comparewith);
212
        $c->update_state($cm, COMPLETION_INCOMPLETE);
213
 
214
        // Auto, change state.
215
        $c = $mockbuilder->getMock();
216
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
217
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
218
        $c->expects($this->once())
219
            ->method('is_enabled')
220
            ->with($cm)
221
            ->will($this->returnValue(true));
222
        $c->expects($this->once())
223
            ->method('get_data')
224
            ->will($this->returnValue($current));
225
        $c->expects($this->once())
226
            ->method('internal_get_state')
227
            ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
228
        $changed = clone($current);
229
        $changed->timemodified = time();
230
        $changed->completionstate = COMPLETION_COMPLETE_PASS;
231
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
232
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
233
        $c->expects($this->once())
234
            ->method('internal_set_data')
235
            ->with($cm, $comparewith);
236
        $c->update_state($cm, COMPLETION_COMPLETE_PASS);
237
 
238
        // Manual tracking, change state by overriding it manually.
239
        $c = $mockbuilder->getMock();
240
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
241
        $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
242
        $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
243
        $c->expects($this->exactly(2))
244
            ->method('is_enabled')
245
            ->with($cm)
246
            ->will($this->returnValue(true));
247
        $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
248
            ->method('user_can_override_completion')
249
            ->will($this->returnValue(true));
250
        $c->expects($this->exactly(2))
251
            ->method('get_data')
252
            ->with($cm, false, 100)
253
            ->willReturnOnConsecutiveCalls($current1, $current2);
254
        $changed1 = clone($current1);
255
        $changed1->timemodified = time();
256
        $changed1->completionstate = COMPLETION_COMPLETE;
257
        $changed1->overrideby = 314159;
258
        $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
259
        $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
260
        $changed2 = clone($current2);
261
        $changed2->timemodified = time();
262
        $changed2->overrideby = null;
263
        $changed2->completionstate = COMPLETION_INCOMPLETE;
264
        $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
265
        $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
266
        $c->expects($this->exactly(2))
267
            ->method('internal_set_data')
268
            ->withConsecutive(
269
                array($cm, $comparewith1),
270
                array($cm, $comparewith2)
271
            );
272
        $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
273
        // And confirm that the status can be changed back to incomplete without an override.
274
        $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
275
 
276
        // Auto, change state via override, incomplete to complete.
277
        $c = $mockbuilder->getMock();
278
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
279
        $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
280
        $c->expects($this->once())
281
            ->method('is_enabled')
282
            ->with($cm)
283
            ->will($this->returnValue(true));
284
        $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
285
            ->method('user_can_override_completion')
286
            ->will($this->returnValue(true));
287
        $c->expects($this->once())
288
            ->method('get_data')
289
            ->with($cm, false, 100)
290
            ->will($this->returnValue($current));
291
        $changed = clone($current);
292
        $changed->timemodified = time();
293
        $changed->completionstate = COMPLETION_COMPLETE;
294
        $changed->overrideby = 314159;
295
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
296
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
297
        $c->expects($this->once())
298
            ->method('internal_set_data')
299
            ->with($cm, $comparewith);
300
        $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
301
 
302
        // Now confirm the status can be changed back from complete to incomplete using an override.
303
        $c = $mockbuilder->getMock();
304
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
305
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
306
        $c->expects($this->once())
307
            ->method('is_enabled')
308
            ->with($cm)
309
            ->will($this->returnValue(true));
310
        $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
311
            ->method('user_can_override_completion')
312
            ->will($this->returnValue(true));
313
        $c->expects($this->once())
314
            ->method('get_data')
315
            ->with($cm, false, 100)
316
            ->will($this->returnValue($current));
317
        $changed = clone($current);
318
        $changed->timemodified = time();
319
        $changed->completionstate = COMPLETION_INCOMPLETE;
320
        $changed->overrideby = 314159;
321
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
322
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
323
        $c->expects($this->once())
324
            ->method('internal_set_data')
325
            ->with($cm, $comparewith);
326
        $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
327
    }
328
 
329
    /**
330
     * Data provider for test_internal_get_state().
331
     *
332
     * @return array[]
333
     */
334
    public function internal_get_state_provider() {
335
        return [
336
            'View required, but not viewed yet' => [
337
                COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
338
            ],
339
            'View not required and not viewed yet' => [
340
                COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
341
            ],
342
            'View not required, grade required but no grade yet, $cm->modname not set' => [
343
                COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
344
            ],
345
            'View not required, grade required but no grade yet, $cm->course not set' => [
346
                COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
347
            ],
348
            'View not required, grade not required' => [
349
                COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
350
            ],
351
        ];
352
    }
353
 
354
    /**
355
     * Test for completion_info::get_state().
356
     *
357
     * @dataProvider internal_get_state_provider
358
     * @param int $completionview
359
     * @param int $completionusegrade
360
     * @param string $unsetfield
361
     * @param int $expectedstate
362
     * @covers ::internal_get_state
363
     */
11 efrain 364
    public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate): void {
1 efrain 365
        $this->setup_data();
366
 
367
        /** @var \mod_assign_generator $assigngenerator */
368
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
369
        $assign = $assigngenerator->create_instance([
370
            'course' => $this->course->id,
371
            'completion' => COMPLETION_ENABLED,
372
            'completionview' => $completionview,
373
            'completionusegrade' => $completionusegrade,
374
        ]);
375
 
376
        $userid = $this->user->id;
377
        $this->setUser($userid);
378
 
379
        $cm = get_coursemodule_from_instance('assign', $assign->id);
380
        if ($unsetfield) {
381
            unset($cm->$unsetfield);
382
        }
383
        // If view is required, but they haven't viewed it yet.
384
        $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
385
 
386
        $completioninfo = new completion_info($this->course);
387
        $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
388
    }
389
 
390
    /**
391
     * Provider for the test_internal_get_state_with_grade_criteria.
392
     *
393
     * @return array
394
     */
395
    public function internal_get_state_with_grade_criteria_provider() {
396
        return [
397
            "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
398
                [
399
                    'completionusegrade' => 1,
400
                    'completionpassgrade' => 1,
401
                    'gradepass' => 50,
402
                ],
403
                50,
404
                COMPLETION_COMPLETE_PASS
405
            ],
406
            "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
407
                [
408
                    'completionusegrade' => 1,
409
                    'completionpassgrade' => 1,
410
                    'gradepass' => 50,
411
                ],
412
                40,
413
                COMPLETION_COMPLETE_FAIL
414
            ],
415
            "Passing grade not enabled with passing grade set." => [
416
                [
417
                    'completionusegrade' => 1,
418
                    'gradepass' => 50,
419
                ],
420
                50,
421
                COMPLETION_COMPLETE_PASS
422
            ],
423
            "Passing grade not enabled with passing grade not set." => [
424
                [
425
                    'completionusegrade' => 1,
426
                ],
427
                90,
428
                COMPLETION_COMPLETE
429
            ],
430
            "Passing grade not enabled with passing grade not set. No submission made." => [
431
                [
432
                    'completionusegrade' => 1,
433
                ],
434
                null,
435
                COMPLETION_INCOMPLETE
436
            ],
437
        ];
438
    }
439
 
440
    /**
441
     * Tests that the right completion state is being set based on the grade criteria.
442
     *
443
     * @dataProvider internal_get_state_with_grade_criteria_provider
444
     * @param array $completioncriteria The completion criteria to use
445
     * @param int|null $studentgrade Grade to assign to student
446
     * @param int $expectedstate Expected completion state
447
     * @covers ::internal_get_state
448
     */
11 efrain 449
    public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?int $studentgrade, int $expectedstate): void {
1 efrain 450
        $this->setup_data();
451
 
452
        /** @var \mod_assign_generator $assigngenerator */
453
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
454
        $assign = $assigngenerator->create_instance([
455
            'course' => $this->course->id,
456
            'completion' => COMPLETION_ENABLED,
457
        ] + $completioncriteria);
458
 
459
        $userid = $this->user->id;
460
 
461
        $cm = get_coursemodule_from_instance('assign', $assign->id);
462
        $usercm = cm_info::create($cm, $userid);
463
 
464
        // Create a teacher account.
465
        $teacher = $this->getDataGenerator()->create_user();
466
        $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
467
        // Log in as the teacher.
468
        $this->setUser($teacher);
469
 
470
        // Grade the student for this assignment.
471
        $assign = new assign($usercm->context, $cm, $cm->course);
472
        if ($studentgrade) {
473
            $data = (object)[
474
                'sendstudentnotifications' => false,
475
                'attemptnumber' => 1,
476
                'grade' => $studentgrade,
477
            ];
478
            $assign->save_grade($userid, $data);
479
        }
480
 
481
        // The target user already received a grade, so internal_get_state should be already complete.
482
        $completioninfo = new completion_info($this->course);
483
        $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
484
    }
485
 
486
    /**
487
     * Covers the case where internal_get_state() is being called for a user different from the logged in user.
488
     *
489
     * @covers ::internal_get_state
490
     */
11 efrain 491
    public function test_internal_get_state_with_different_user(): void {
1 efrain 492
        $this->setup_data();
493
 
494
        /** @var \mod_assign_generator $assigngenerator */
495
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
496
        $assign = $assigngenerator->create_instance([
497
            'course' => $this->course->id,
498
            'completion' => COMPLETION_ENABLED,
499
            'completionusegrade' => 1,
500
        ]);
501
 
502
        $userid = $this->user->id;
503
 
504
        $cm = get_coursemodule_from_instance('assign', $assign->id);
505
        $usercm = cm_info::create($cm, $userid);
506
 
507
        // Create a teacher account.
508
        $teacher = $this->getDataGenerator()->create_user();
509
        $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
510
        // Log in as the teacher.
511
        $this->setUser($teacher);
512
 
513
        // Grade the student for this assignment.
514
        $assign = new assign($usercm->context, $cm, $cm->course);
515
        $data = (object)[
516
            'sendstudentnotifications' => false,
517
            'attemptnumber' => 1,
518
            'grade' => 90,
519
        ];
520
        $assign->save_grade($userid, $data);
521
 
522
        // The target user already received a grade, so internal_get_state should be already complete.
523
        $completioninfo = new completion_info($this->course);
524
        $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
525
 
526
        // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
527
        $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
528
    }
529
 
530
    /**
531
     * Test for internal_get_state() for an activity that supports custom completion.
532
     *
533
     * @covers ::internal_get_state
534
     */
11 efrain 535
    public function test_internal_get_state_with_custom_completion(): void {
1 efrain 536
        $this->setup_data();
537
 
538
        $choicerecord = [
539
            'course' => $this->course,
540
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
541
            'completionsubmit' => COMPLETION_ENABLED,
542
        ];
543
        $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
544
        $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
545
 
546
        $completioninfo = new completion_info($this->course);
547
 
548
        // Fetch completion for the user who hasn't made a choice yet.
549
        $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
550
        $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
551
 
552
        // Have the user make a choice.
553
        $choicewithoptions = choice_get_choice($choice->id);
554
        $optionids = array_keys($choicewithoptions->option);
555
        choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
556
        $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
557
        $this->assertEquals(COMPLETION_COMPLETE, $completion);
558
    }
559
 
560
    /**
561
     * @covers ::set_module_viewed
562
     */
11 efrain 563
    public function test_set_module_viewed(): void {
1 efrain 564
        $this->mock_setup();
565
 
566
        $mockbuilder = $this->getMockBuilder('completion_info');
567
        $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
568
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
569
        $cm = (object)array('id' => 13, 'course' => 42);
570
 
571
        // Not tracking completion, should do nothing.
572
        $c = $mockbuilder->getMock();
573
        $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
574
        $c->set_module_viewed($cm);
575
 
576
        // Tracking completion but completion is disabled, should do nothing.
577
        $c = $mockbuilder->getMock();
578
        $cm->completionview = COMPLETION_VIEW_REQUIRED;
579
        $c->expects($this->once())
580
            ->method('is_enabled')
581
            ->with($cm)
582
            ->will($this->returnValue(false));
583
        $c->set_module_viewed($cm);
584
 
585
        // Now it's enabled, we expect it to get data. If data already has
586
        // viewed, still do nothing.
587
        $c = $mockbuilder->getMock();
588
        $c->expects($this->once())
589
            ->method('is_enabled')
590
            ->with($cm)
591
            ->will($this->returnValue(true));
592
        $c->expects($this->once())
593
            ->method('get_data')
594
            ->with($cm, 0)
595
            ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
596
        $c->set_module_viewed($cm);
597
 
598
        // OK finally one that hasn't been viewed, now it should set it viewed
599
        // and update state.
600
        $c = $mockbuilder->getMock();
601
        $c->expects($this->once())
602
            ->method('is_enabled')
603
            ->with($cm)
604
            ->will($this->returnValue(true));
605
        $c->expects($this->once())
606
            ->method('get_data')
607
            ->with($cm, false, 1337)
608
            ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
609
        $c->expects($this->once())
610
            ->method('internal_set_data')
611
            ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
612
        $c->expects($this->once())
613
            ->method('update_state')
614
            ->with($cm, COMPLETION_COMPLETE, 1337);
615
        $c->set_module_viewed($cm, 1337);
616
    }
617
 
618
    /**
619
     * @covers ::count_user_data
620
     */
11 efrain 621
    public function test_count_user_data(): void {
1 efrain 622
        global $DB;
623
        $this->mock_setup();
624
 
625
        $course = (object)array('id' => 13);
626
        $cm = (object)array('id' => 42);
627
 
628
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
629
        $DB->expects($this->once())
630
            ->method('get_field_sql')
631
            ->will($this->returnValue(666));
632
 
633
        $c = new completion_info($course);
634
        $this->assertEquals(666, $c->count_user_data($cm));
635
    }
636
 
637
    /**
638
     * @covers ::delete_all_state
639
     */
11 efrain 640
    public function test_delete_all_state(): void {
1 efrain 641
        global $DB;
642
        $this->mock_setup();
643
 
644
        $course = (object)array('id' => 13);
645
        $cm = (object)array('id' => 42, 'course' => 13);
646
        $c = new completion_info($course);
647
 
648
        // Check it works ok without data in session.
649
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
650
        $DB->expects($this->once())
651
            ->method('delete_records')
652
            ->with('course_modules_completion', array('coursemoduleid' => 42))
653
            ->will($this->returnValue(true));
654
        $c->delete_all_state($cm);
655
    }
656
 
657
    /**
658
     * @covers ::reset_all_state
659
     */
11 efrain 660
    public function test_reset_all_state(): void {
1 efrain 661
        global $DB;
662
        $this->mock_setup();
663
 
664
        $mockbuilder = $this->getMockBuilder('completion_info');
665
        $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
666
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
667
        $c = $mockbuilder->getMock();
668
 
669
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
670
 
671
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
672
        $DB->expects($this->once())
673
            ->method('get_recordset')
674
            ->will($this->returnValue(
675
                new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
676
                    (object)array('id' => 2, 'userid' => 101)))));
677
 
678
        $c->expects($this->once())
679
            ->method('delete_all_state')
680
            ->with($cm);
681
 
682
        $c->expects($this->once())
683
            ->method('get_tracked_users')
684
            ->will($this->returnValue(array(
685
            (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
686
            (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
687
 
688
        $c->expects($this->exactly(3))
689
            ->method('update_state')
690
            ->withConsecutive(
691
                array($cm, COMPLETION_UNKNOWN, 100),
692
                array($cm, COMPLETION_UNKNOWN, 101),
693
                array($cm, COMPLETION_UNKNOWN, 201)
694
            );
695
 
696
        $c->reset_all_state($cm);
697
    }
698
 
699
    /**
700
     * Data provider for test_get_data().
701
     *
702
     * @return array[]
703
     */
704
    public function get_data_provider() {
705
        return [
706
            'No completion record' => [
707
                false, true, false, COMPLETION_INCOMPLETE
708
            ],
709
            'Not completed' => [
710
                false, true, true, COMPLETION_INCOMPLETE
711
            ],
712
            'Completed' => [
713
                false, true, true, COMPLETION_COMPLETE
714
            ],
715
            'Whole course, complete' => [
716
                true, true, true, COMPLETION_COMPLETE
717
            ],
718
            'Get data for another user, result should be not cached' => [
719
                false, false, true,  COMPLETION_INCOMPLETE
720
            ],
721
            'Get data for another user, including whole course, result should be not cached' => [
722
                true, false, true,  COMPLETION_INCOMPLETE
723
            ],
724
        ];
725
    }
726
 
727
    /**
728
     * Tests for completion_info::get_data().
729
     *
730
     * @dataProvider get_data_provider
731
     * @param bool $wholecourse Whole course parameter for get_data().
732
     * @param bool $sameuser Whether the user calling get_data() is the user itself.
733
     * @param bool $hasrecord Whether to create a course_modules_completion record.
734
     * @param int $completion The completion state expected.
735
     * @covers ::get_data
736
     */
11 efrain 737
    public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion): void {
1 efrain 738
        global $DB;
739
 
740
        $this->setup_data();
741
        $user = $this->user;
742
 
743
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
744
        $choice = $choicegenerator->create_instance([
745
            'course' => $this->course->id,
746
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
747
            'completionview' => true,
748
            'completionsubmit' => true,
749
        ]);
750
 
751
        $cm = get_coursemodule_from_instance('choice', $choice->id);
752
 
753
        // Let's manually create a course completion record instead of going through the hoops to complete an activity.
754
        if ($hasrecord) {
755
            $cmcompletionrecord = (object)[
756
                'coursemoduleid' => $cm->id,
757
                'userid' => $user->id,
758
                'completionstate' => $completion,
759
                'overrideby' => null,
760
                'timemodified' => 0,
761
            ];
762
            $cmcompletionviewrecord = (object)[
763
                'coursemoduleid' => $cm->id,
764
                'userid' => $user->id,
765
                'timecreated' => 0,
766
            ];
767
            $DB->insert_record('course_modules_completion', $cmcompletionrecord);
768
            $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
769
        }
770
 
771
        // Whether we expect for the returned completion data to be stored in the cache.
772
        $iscached = true;
773
 
774
        if (!$sameuser) {
775
            $iscached = false;
776
            $this->setAdminUser();
777
        } else {
778
            $this->setUser($user);
779
        }
780
 
781
        // Mock other completion data.
782
        $completioninfo = new completion_info($this->course);
783
 
784
        $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
785
 
786
        // Course module ID of the returned completion data must match this activity's course module ID.
787
        $this->assertEquals($cm->id, $result->coursemoduleid);
788
        // User ID of the returned completion data must match the user's ID.
789
        $this->assertEquals($user->id, $result->userid);
790
        // The completion state of the returned completion data must match the expected completion state.
791
        $this->assertEquals($completion, $result->completionstate);
792
 
793
        // If the user has no completion record, then the default record should be returned.
794
        if (!$hasrecord) {
795
            $this->assertEquals(0, $result->id);
796
        }
797
 
798
        // Check that we are including relevant completion data for the module.
799
        if (!$wholecourse) {
800
            $this->assertTrue(property_exists($result, 'viewed'));
801
            $this->assertTrue(property_exists($result, 'customcompletion'));
802
        }
803
    }
804
 
805
    /**
806
     * @covers ::get_data
807
     */
808
    public function test_get_data_successive_calls(): void {
809
        global $DB;
810
 
811
        $this->setup_data();
812
        $this->setUser($this->user);
813
 
814
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
815
        $choice = $choicegenerator->create_instance([
816
            'course' => $this->course->id,
817
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
818
            'completionview' => true,
819
            'completionsubmit' => true,
820
        ]);
821
 
822
        $cm = get_coursemodule_from_instance('choice', $choice->id);
823
 
824
        // Let's manually create a course completion record instead of going through the hoops to complete an activity.
825
        $cmcompletionrecord = (object) [
826
            'coursemoduleid' => $cm->id,
827
            'userid' => $this->user->id,
828
            'completionstate' => COMPLETION_NOT_VIEWED,
829
            'overrideby' => null,
830
            'timemodified' => 0,
831
        ];
832
        $cmcompletionviewrecord = (object)[
833
            'coursemoduleid' => $cm->id,
834
            'userid' => $this->user->id,
835
            'timecreated' => 0,
836
        ];
837
        $DB->insert_record('course_modules_completion', $cmcompletionrecord);
838
        $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
839
 
840
        // Mock other completion data.
841
        $completioninfo = new completion_info($this->course);
842
 
843
        $modinfo = get_fast_modinfo($this->course);
844
        $results = [];
845
        foreach ($modinfo->cms as $testcm) {
846
            $result = $completioninfo->get_data($testcm, true);
847
            $this->assertTrue(property_exists($result, 'id'));
848
            $this->assertTrue(property_exists($result, 'coursemoduleid'));
849
            $this->assertTrue(property_exists($result, 'userid'));
850
            $this->assertTrue(property_exists($result, 'completionstate'));
851
            $this->assertTrue(property_exists($result, 'viewed'));
852
            $this->assertTrue(property_exists($result, 'overrideby'));
853
            $this->assertTrue(property_exists($result, 'timemodified'));
854
            $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
855
 
856
            $this->assertEquals($testcm->id, $result->coursemoduleid);
857
            $this->assertEquals($this->user->id, $result->userid);
858
 
859
            $results[$testcm->id] = $result;
860
        }
861
 
862
        $result = $completioninfo->get_data($cm);
863
        $this->assertTrue(property_exists($result, 'customcompletion'));
864
 
865
        // The data should match when fetching modules individually.
866
        (cache::make('core', 'completion'))->purge();
867
        foreach ($modinfo->cms as $testcm) {
868
            $result = $completioninfo->get_data($testcm, false);
869
            $this->assertEquals($result, $results[$testcm->id]);
870
        }
871
    }
872
 
873
    /**
874
     * Tests for get_completion_data().
875
     *
876
     * @covers ::get_completion_data
877
     */
11 efrain 878
    public function test_get_completion_data(): void {
879
        $this->setAdminUser();
1 efrain 880
        $this->setup_data();
881
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
882
        $choice = $choicegenerator->create_instance([
883
            'course' => $this->course->id,
884
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
885
            'completionview' => true,
886
            'completionsubmit' => true,
887
        ]);
888
        $cm = get_coursemodule_from_instance('choice', $choice->id);
889
 
890
        // Mock other completion data.
891
        $completioninfo = new completion_info($this->course);
892
        // Default data to return when no completion data is found.
893
        $defaultdata = [
894
            'id' => 0,
895
            'coursemoduleid' => $cm->id,
896
            'userid' => $this->user->id,
897
            'completionstate' => 0,
898
            'viewed' => 0,
899
            'overrideby' => null,
900
            'timemodified' => 0,
901
        ];
902
 
903
        $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
904
        $this->assertTrue(array_key_exists('viewed', $completiondatabeforeview));
905
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondatabeforeview));
906
        $this->assertEquals(0, $completiondatabeforeview['viewed']);
907
        $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
908
 
11 efrain 909
        // Mark as completed before viewing it.
910
        $completioninfo->update_state($cm, COMPLETION_COMPLETE, $this->user->id, true);
911
        $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
912
        $this->assertEquals(0, $completiondatabeforeview['viewed']);
913
 
1 efrain 914
        // Set viewed.
915
        $completioninfo->set_module_viewed($cm, $this->user->id);
916
 
917
        $completiondata = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
918
        $this->assertTrue(array_key_exists('viewed', $completiondata));
919
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondata));
920
        $this->assertEquals(1, $completiondata['viewed']);
921
        $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
922
 
923
        $completioninfo->reset_all_state($cm);
924
 
925
        $completiondataafterreset = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
926
        $this->assertTrue(array_key_exists('viewed', $completiondataafterreset));
927
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondataafterreset));
928
        $this->assertEquals(1, $completiondataafterreset['viewed']);
929
        $this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
930
    }
931
 
932
    /**
933
     * Tests for completion_info::get_other_cm_completion_data().
934
     *
935
     * @covers ::get_other_cm_completion_data
936
     */
11 efrain 937
    public function test_get_other_cm_completion_data(): void {
1 efrain 938
        global $DB;
939
 
940
        $this->setup_data();
941
        $user = $this->user;
942
 
943
        $this->setAdminUser();
944
 
945
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
946
        $choice = $choicegenerator->create_instance([
947
            'course' => $this->course->id,
948
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
949
            'completionsubmit' => true,
950
        ]);
951
 
952
        $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
953
 
954
        $choice2 = $choicegenerator->create_instance([
955
            'course' => $this->course->id,
956
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
957
        ]);
958
 
959
        $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
960
 
961
        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
962
        $workshop = $workshopgenerator->create_instance([
963
            'course' => $this->course->id,
964
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
965
            // Submission grade required.
966
            'completiongradeitemnumber' => 0,
967
            'completionpassgrade' => 1,
968
        ]);
969
 
970
        $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
971
 
972
        $completioninfo = new completion_info($this->course);
973
 
974
        $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
975
 
976
        // Check that fetching data for a module with custom completion provides its info.
977
        $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
978
 
979
        $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
980
        $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
981
        $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
982
 
983
        // Mock a choice answer so user has completed the requirement.
984
        $choicemockinfo = [
985
            'choiceid' => $cmchoice->instance,
986
            'userid' => $this->user->id
987
        ];
988
        $DB->insert_record('choice_answers', $choicemockinfo, false);
989
 
990
        // Confirm fetching again reflects the completion.
991
        $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
992
        $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
993
 
994
        // Check that fetching data for a module with no custom completion still provides its grade completion status.
995
        $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
996
 
997
        $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
998
        $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
999
        $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
1000
        $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
1001
        $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']);
1002
 
1003
        // Check that fetching data for a module with no completion conditions does not provide any data.
1004
        $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
1005
        $this->assertEmpty($choice2completiondata);
1006
    }
1007
 
1008
    /**
1009
     * @covers ::internal_set_data
1010
     */
11 efrain 1011
    public function test_internal_set_data(): void {
1 efrain 1012
        global $DB;
1013
        $this->setup_data();
1014
 
1015
        $this->setUser($this->user);
1016
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1017
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1018
        $cm = get_coursemodule_from_instance('forum', $forum->id);
1019
        $c = new completion_info($this->course);
1020
 
1021
        // 1) Test with new data.
1022
        $data = new stdClass();
1023
        $data->id = 0;
1024
        $data->userid = $this->user->id;
1025
        $data->coursemoduleid = $cm->id;
1026
        $data->completionstate = COMPLETION_COMPLETE;
1027
        $data->timemodified = time();
1028
        $data->viewed = COMPLETION_NOT_VIEWED;
1029
        $data->overrideby = null;
1030
 
1031
        $c->internal_set_data($cm, $data);
1032
        $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
1033
        $this->assertEquals($d1, $data->id);
1034
        $cache = cache::make('core', 'completion');
1035
        // Cache was not set for another user.
1036
        $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
1037
        $this->assertEquals([
1038
            'cacherev' => $this->course->cacherev,
1039
            $cm->id => array_merge(
1040
                (array) $data,
1041
                ['other_cm_completion_data_fetched' => true]
1042
            ),
1043
        ],
1044
        $cachevalue);
1045
 
1046
        // 2) Test with existing data and for different user.
1047
        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1048
        $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
1049
        $newuser = $this->getDataGenerator()->create_user();
1050
 
1051
        $d2 = new stdClass();
1052
        $d2->id = 7;
1053
        $d2->userid = $newuser->id;
1054
        $d2->coursemoduleid = $cm2->id;
1055
        $d2->completionstate = COMPLETION_COMPLETE;
1056
        $d2->timemodified = time();
1057
        $d2->viewed = COMPLETION_NOT_VIEWED;
1058
        $d2->overrideby = null;
1059
        $c->internal_set_data($cm2, $d2);
1060
        // Cache for current user returns the data.
1061
        $cachevalue = $cache->get($data->userid . '_' . $cm->course);
1062
        $this->assertEquals(array_merge(
1063
            (array) $data,
1064
            ['other_cm_completion_data_fetched' => true]
1065
        ), $cachevalue[$cm->id]);
1066
 
1067
        // Cache for another user is not filled.
1068
        $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
1069
 
1070
        // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1071
        $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1072
        $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
1073
        $newuser2 = $this->getDataGenerator()->create_user();
1074
        $d3 = new stdClass();
1075
        $d3->id = 13;
1076
        $d3->userid = $newuser2->id;
1077
        $d3->coursemoduleid = $cm3->id;
1078
        $d3->completionstate = COMPLETION_COMPLETE;
1079
        $d3->timemodified = time();
1080
        $d3->viewed = COMPLETION_NOT_VIEWED;
1081
        $d3->overrideby = null;
1082
        $DB->insert_record('course_modules_completion', $d3);
1083
        $c->internal_set_data($cm, $data);
1084
 
1085
        // 4) Test instant course completions.
1086
        $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id),
1087
            array('completion' => 1));
1088
        $cm = get_coursemodule_from_instance('data', $dataactivity->id);
1089
        $c = new completion_info($this->course);
1090
        $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid);
1091
 
1092
        // Add activity completion criteria.
1093
        $criteriadata = new stdClass();
1094
        $criteriadata->id = $this->course->id;
1095
        $criteriadata->criteria_activity = array();
1096
        // Some activities.
1097
        $criteriadata->criteria_activity[$cmdata->id] = 1;
1098
        $class = 'completion_criteria_activity';
1099
        $criterion = new $class();
1100
        $criterion->update_config($criteriadata);
1101
 
1102
        $actual = $DB->get_records('course_completions');
1103
        $this->assertEmpty($actual);
1104
 
1105
        $data->coursemoduleid = $cm->id;
1106
        $c->internal_set_data($cm, $data);
1107
        $actual = $DB->get_records('course_completions');
1108
        $this->assertEquals(1, count($actual));
1109
        $this->assertEquals($this->user->id, reset($actual)->userid);
1110
 
1111
        $data->userid = $newuser2->id;
1112
        $c->internal_set_data($cm, $data, true);
1113
        $actual = $DB->get_records('course_completions');
1114
        $this->assertEquals(1, count($actual));
1115
        $this->assertEquals($this->user->id, reset($actual)->userid);
1116
    }
1117
 
1118
    /**
1119
     * @covers ::get_progress_all
1120
     */
11 efrain 1121
    public function test_get_progress_all_few(): void {
1 efrain 1122
        global $DB;
1123
        $this->mock_setup();
1124
 
1125
        $mockbuilder = $this->getMockBuilder('completion_info');
1126
        $mockbuilder->onlyMethods(array('get_tracked_users'));
1127
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1128
        $c = $mockbuilder->getMock();
1129
 
1130
        // With few results.
1131
        $c->expects($this->once())
1132
            ->method('get_tracked_users')
1133
            ->with(false,  array(),  0,  '',  '',  '',  null)
1134
            ->will($this->returnValue(array(
1135
                (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1136
                (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1137
        $DB->expects($this->once())
1138
            ->method('get_in_or_equal')
1139
            ->with(array(100, 201))
1140
            ->will($this->returnValue(array(' IN (100, 201)', array())));
1141
        $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1142
        $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1143
        $DB->expects($this->once())
1144
            ->method('get_recordset_sql')
1145
            ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1146
 
1147
        $this->assertEquals(array(
1148
                100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1149
                    'progress' => array(13 => $progress1)),
1150
                201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1151
                    'progress' => array(14 => $progress2)),
1152
            ), $c->get_progress_all(false));
1153
    }
1154
 
1155
    /**
1156
     * @covers ::get_progress_all
1157
     */
11 efrain 1158
    public function test_get_progress_all_lots(): void {
1 efrain 1159
        global $DB;
1160
        $this->mock_setup();
1161
 
1162
        $mockbuilder = $this->getMockBuilder('completion_info');
1163
        $mockbuilder->onlyMethods(array('get_tracked_users'));
1164
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1165
        $c = $mockbuilder->getMock();
1166
 
1167
        $tracked = array();
1168
        $ids = array();
1169
        $progress = array();
1170
        // With more than 1000 results.
1171
        for ($i = 100; $i < 2000; $i++) {
1172
            $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1173
            $ids[] = $i;
1174
            $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1175
            $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1176
        }
1177
        $c->expects($this->once())
1178
            ->method('get_tracked_users')
1179
            ->with(true,  3,  0,  '',  '',  '',  null)
1180
            ->will($this->returnValue($tracked));
1181
        $DB->expects($this->exactly(2))
1182
            ->method('get_in_or_equal')
1183
            ->withConsecutive(
1184
                array(array_slice($ids, 0, 1000)),
1185
                array(array_slice($ids, 1000))
1186
            )
1187
            ->willReturnOnConsecutiveCalls(
1188
                array(' IN whatever', array()),
1189
                array(' IN whatever2', array()));
1190
        $DB->expects($this->exactly(2))
1191
            ->method('get_recordset_sql')
1192
            ->willReturnOnConsecutiveCalls(
1193
                new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1194
                new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1195
 
1196
        $result = $c->get_progress_all(true, 3);
1197
        $resultok = true;
1198
        $resultok = $resultok && ($ids == array_keys($result));
1199
 
1200
        foreach ($result as $userid => $data) {
1201
            $resultok = $resultok && $data->firstname == 'frog';
1202
            $resultok = $resultok && $data->lastname == $userid;
1203
            $resultok = $resultok && $data->id == $userid;
1204
            $cms = $data->progress;
1205
            $resultok = $resultok && (array(13, 14) == array_keys($cms));
1206
            $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1207
            $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1208
        }
1209
        $this->assertTrue($resultok);
1210
        $this->assertCount(count($tracked), $result);
1211
    }
1212
 
1213
    /**
1214
     * @covers ::inform_grade_changed
1215
     */
11 efrain 1216
    public function test_inform_grade_changed(): void {
1 efrain 1217
        $this->mock_setup();
1218
 
1219
        $mockbuilder = $this->getMockBuilder('completion_info');
1220
        $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1221
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1222
 
1223
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1224
        $item = (object)array('itemnumber' => 3,  'gradepass' => 1,  'hidden' => 0);
1225
        $grade = (object)array('userid' => 31337,  'finalgrade' => 0,  'rawgrade' => 0);
1226
 
1227
        // Not enabled (should do nothing).
1228
        $c = $mockbuilder->getMock();
1229
        $c->expects($this->once())
1230
            ->method('is_enabled')
1231
            ->with($cm)
1232
            ->will($this->returnValue(false));
1233
        $c->inform_grade_changed($cm, $item, $grade, false);
1234
 
1235
        // Enabled but still no grade completion required,  should still do nothing.
1236
        $c = $mockbuilder->getMock();
1237
        $c->expects($this->once())
1238
            ->method('is_enabled')
1239
            ->with($cm)
1240
            ->will($this->returnValue(true));
1241
        $c->inform_grade_changed($cm, $item, $grade, false);
1242
 
1243
        // Enabled and completion required but item number is wrong,  does nothing.
1244
        $c = $mockbuilder->getMock();
1245
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1246
        $c->expects($this->once())
1247
            ->method('is_enabled')
1248
            ->with($cm)
1249
            ->will($this->returnValue(true));
1250
        $c->inform_grade_changed($cm, $item, $grade, false);
1251
 
1252
        // Enabled and completion required and item number right. It is supposed
1253
        // to call update_state with the new potential state being obtained from
1254
        // internal_get_grade_state.
1255
        $c = $mockbuilder->getMock();
1256
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1257
        $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1258
        $c->expects($this->once())
1259
            ->method('is_enabled')
1260
            ->with($cm)
1261
            ->will($this->returnValue(true));
1262
        $c->expects($this->once())
1263
            ->method('update_state')
1264
            ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1265
            ->will($this->returnValue(true));
1266
        $c->inform_grade_changed($cm, $item, $grade, false);
1267
 
1268
        // Same as above but marked deleted. It is supposed to call update_state
1269
        // with new potential state being COMPLETION_INCOMPLETE.
1270
        $c = $mockbuilder->getMock();
1271
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1272
        $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1273
        $c->expects($this->once())
1274
            ->method('is_enabled')
1275
            ->with($cm)
1276
            ->will($this->returnValue(true));
1277
        $c->expects($this->once())
1278
            ->method('update_state')
1279
            ->with($cm, COMPLETION_INCOMPLETE, 31337)
1280
            ->will($this->returnValue(true));
1281
        $c->inform_grade_changed($cm, $item, $grade, true);
1282
    }
1283
 
1284
    /**
1285
     * @covers ::internal_get_grade_state
1286
     */
11 efrain 1287
    public function test_internal_get_grade_state(): void {
1 efrain 1288
        $this->mock_setup();
1289
 
1290
        $item = new stdClass;
1291
        $grade = new stdClass;
1292
 
1293
        $item->gradepass = 4;
1294
        $item->hidden = 0;
1295
        $grade->rawgrade = 4.0;
1296
        $grade->finalgrade = null;
1297
 
1298
        // Grade has pass mark and is not hidden,  user passes.
1299
        $this->assertEquals(
1300
            COMPLETION_COMPLETE_PASS,
1301
            completion_info::internal_get_grade_state($item, $grade));
1302
 
1303
        // Same but user fails.
1304
        $grade->rawgrade = 3.9;
1305
        $this->assertEquals(
1306
            COMPLETION_COMPLETE_FAIL,
1307
            completion_info::internal_get_grade_state($item, $grade));
1308
 
1309
        // User fails on raw grade but passes on final.
1310
        $grade->finalgrade = 4.0;
1311
        $this->assertEquals(
1312
            COMPLETION_COMPLETE_PASS,
1313
            completion_info::internal_get_grade_state($item, $grade));
1314
 
1315
        // Item is hidden.
1316
        $item->hidden = 1;
1317
        $this->assertEquals(
1318
            COMPLETION_COMPLETE,
1319
            completion_info::internal_get_grade_state($item, $grade));
1320
 
1321
        // Item isn't hidden but has no pass mark.
1322
        $item->hidden = 0;
1323
        $item->gradepass = 0;
1324
        $this->assertEquals(
1325
            COMPLETION_COMPLETE,
1326
            completion_info::internal_get_grade_state($item, $grade));
1327
 
1328
        // Item is hidden, but returnpassfail is true and the grade is passing.
1329
        $item->hidden = 1;
1330
        $item->gradepass = 4;
1331
        $grade->finalgrade = 5.0;
1332
        $this->assertEquals(
1333
            COMPLETION_COMPLETE_PASS,
1334
            completion_info::internal_get_grade_state($item, $grade, true));
1335
 
1336
        // Item is hidden, but returnpassfail is true and the grade is failing.
1337
        $item->hidden = 1;
1338
        $item->gradepass = 4;
1339
        $grade->finalgrade = 3.0;
1340
        $this->assertEquals(
1341
            COMPLETION_COMPLETE_FAIL_HIDDEN,
1342
            completion_info::internal_get_grade_state($item, $grade, true));
1343
 
1344
        // Item is not hidden, but returnpassfail is true and the grade is failing.
1345
        $item->hidden = 0;
1346
        $item->gradepass = 4;
1347
        $grade->finalgrade = 3.0;
1348
        $this->assertEquals(
1349
            COMPLETION_COMPLETE_FAIL,
1350
            completion_info::internal_get_grade_state($item, $grade, true));
1351
    }
1352
 
1353
    /**
1354
     * @test ::get_activities
1355
     */
11 efrain 1356
    public function test_get_activities(): void {
1 efrain 1357
        global $CFG;
1358
        $this->resetAfterTest();
1359
 
1360
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
1361
        $CFG->enablecompletion = true;
1362
 
1363
        // Create a course with mixed auto completion data.
1364
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1365
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1366
        $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1367
        $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1368
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1369
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1370
        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1371
 
1372
        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1373
        $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1374
        $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1375
 
1376
        // Create data in another course to make sure it's not considered.
1377
        $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1378
        $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1379
        $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1380
        $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1381
 
1382
        $c = new completion_info($course);
1383
        $activities = $c->get_activities();
1384
        $this->assertCount(3, $activities);
1385
        $this->assertTrue(isset($activities[$forum->cmid]));
1386
        $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1387
        $this->assertTrue(isset($activities[$page->cmid]));
1388
        $this->assertSame($page->name, $activities[$page->cmid]->name);
1389
        $this->assertTrue(isset($activities[$data->cmid]));
1390
        $this->assertSame($data->name, $activities[$data->cmid]->name);
1391
 
1392
        $this->assertFalse(isset($activities[$forum2->cmid]));
1393
        $this->assertFalse(isset($activities[$page2->cmid]));
1394
        $this->assertFalse(isset($activities[$data2->cmid]));
1395
    }
1396
 
1397
    /**
1398
     * @test ::has_activities
1399
     */
11 efrain 1400
    public function test_has_activities(): void {
1 efrain 1401
        global $CFG;
1402
        $this->resetAfterTest();
1403
 
1404
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
1405
        $CFG->enablecompletion = true;
1406
 
1407
        // Create a course with mixed auto completion data.
1408
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1409
        $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1410
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1411
        $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1412
        $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1413
        $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1414
 
1415
        $c1 = new completion_info($course);
1416
        $c2 = new completion_info($course2);
1417
 
1418
        $this->assertTrue($c1->has_activities());
1419
        $this->assertFalse($c2->has_activities());
1420
    }
1421
 
1422
    /**
1423
     * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1424
     *
1425
     * @covers ::delete_course_completion_data
1426
     * @covers ::delete_all_completion_data
1427
     */
11 efrain 1428
    public function test_course_delete_prerequisite(): void {
1 efrain 1429
        global $DB;
1430
 
1431
        $this->setup_data();
1432
 
1433
        $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1434
 
1435
        $criteriadata = (object) [
1436
            'id' => $this->course->id,
1437
            'criteria_course' => [$courseprerequisite->id],
1438
        ];
1439
 
1440
        /** @var completion_criteria_course $criteria */
1441
        $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1442
        $criteria->update_config($criteriadata);
1443
 
1444
        // Sanity test.
1445
        $this->assertTrue($DB->record_exists('course_completion_criteria', [
1446
            'course' => $this->course->id,
1447
            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1448
            'courseinstance' => $courseprerequisite->id,
1449
        ]));
1450
 
1451
        // Deleting the prerequisite course should remove the completion criteria.
1452
        delete_course($courseprerequisite, false);
1453
 
1454
        $this->assertFalse($DB->record_exists('course_completion_criteria', [
1455
            'course' => $this->course->id,
1456
            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1457
            'courseinstance' => $courseprerequisite->id,
1458
        ]));
1459
    }
1460
 
1461
    /**
1462
     * Test course module completion update event.
1463
     *
1464
     * @covers \core\event\course_module_completion_updated
1465
     */
11 efrain 1466
    public function test_course_module_completion_updated_event(): void {
1 efrain 1467
        global $USER, $CFG;
1468
 
1469
        $this->setup_data();
1470
 
1471
        $this->setAdminUser();
1472
 
1473
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1474
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1475
 
1476
        $c = new completion_info($this->course);
1477
        $activities = $c->get_activities();
1478
        $this->assertEquals(1, count($activities));
1479
        $this->assertTrue(isset($activities[$forum->cmid]));
1480
        $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1481
 
1482
        $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1483
        $current->completionstate = COMPLETION_COMPLETE;
1484
        $current->timemodified = time();
1485
        $sink = $this->redirectEvents();
1486
        $c->internal_set_data($activities[$forum->cmid], $current);
1487
        $events = $sink->get_events();
1488
        $event = reset($events);
1489
        $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1490
        $this->assertEquals($forum->cmid,
1491
            $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1492
        $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1493
        $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1494
        $this->assertEquals($USER->id, $event->userid);
1495
        $this->assertEquals($this->user->id, $event->relateduserid);
1496
        $this->assertInstanceOf('moodle_url', $event->get_url());
1497
    }
1498
 
1499
    /**
1500
     * Test course completed event.
1501
     *
1502
     * @covers \core\event\course_completed
1503
     */
11 efrain 1504
    public function test_course_completed_event(): void {
1 efrain 1505
        global $USER;
1506
 
1507
        $this->setup_data();
1508
        $this->setAdminUser();
1509
 
1510
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1511
        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1512
 
1513
        // Mark course as complete and get triggered event.
1514
        $sink = $this->redirectEvents();
1515
        $ccompletion->mark_complete();
1516
        $events = $sink->get_events();
1517
        $event = reset($events);
1518
 
1519
        $this->assertInstanceOf('\core\event\course_completed', $event);
1520
        $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1521
        $this->assertEquals($this->course->id, $event->courseid);
1522
        $this->assertEquals($USER->id, $event->userid);
1523
        $this->assertEquals($this->user->id, $event->relateduserid);
1524
        $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1525
        $this->assertInstanceOf('moodle_url', $event->get_url());
1526
    }
1527
 
1528
    /**
1529
     * Test course completed message.
1530
     *
1531
     * @covers \core\event\course_completed
1532
     */
11 efrain 1533
    public function test_course_completed_message(): void {
1 efrain 1534
        $this->setup_data();
1535
        $this->setAdminUser();
1536
 
1537
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1538
        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1539
 
1540
        // Mark course as complete and get the message.
1541
        $sink = $this->redirectMessages();
1542
        $ccompletion->mark_complete();
1543
        $messages = $sink->get_messages();
1544
        $sink->close();
1545
 
1546
        $this->assertCount(1, $messages);
1547
        $message = array_pop($messages);
1548
 
1549
        $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1550
        $this->assertEquals($this->user->id, $message->useridto);
1551
        $this->assertEquals('coursecompleted', $message->eventtype);
1552
        $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1553
        $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1554
    }
1555
 
1556
    /**
1557
     * Test course completed event.
1558
     *
1559
     * @covers \core\event\course_completion_updated
1560
     */
11 efrain 1561
    public function test_course_completion_updated_event(): void {
1 efrain 1562
        $this->setup_data();
1563
        $coursecontext = context_course::instance($this->course->id);
1564
        $coursecompletionevent = \core\event\course_completion_updated::create(
1565
                array(
1566
                    'courseid' => $this->course->id,
1567
                    'context' => $coursecontext
1568
                    )
1569
                );
1570
 
1571
        // Mark course as complete and get triggered event.
1572
        $sink = $this->redirectEvents();
1573
        $coursecompletionevent->trigger();
1574
        $events = $sink->get_events();
1575
        $event = array_pop($events);
1576
        $sink->close();
1577
 
1578
        $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1579
        $this->assertEquals($this->course->id, $event->courseid);
1580
        $this->assertEquals($coursecontext, $event->get_context());
1581
        $this->assertInstanceOf('moodle_url', $event->get_url());
1582
    }
1583
 
1584
    /**
1585
     * @covers \completion_can_view_data
1586
     */
11 efrain 1587
    public function test_completion_can_view_data(): void {
1 efrain 1588
        $this->setup_data();
1589
 
1590
        $student = $this->getDataGenerator()->create_user();
1591
        $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1592
 
1593
        $this->setUser($student);
1594
        $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1595
        $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1596
    }
1597
 
1598
    /**
1599
     * Data provider for test_get_grade_completion().
1600
     *
1601
     * @return array[]
1602
     */
1603
    public function get_grade_completion_provider() {
1604
        return [
1605
            'Grade not required' => [false, false, null, null, null],
1606
            'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1607
            'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1608
            'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1609
            'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1610
        ];
1611
    }
1612
 
1613
    /**
1614
     * Test for \completion_info::get_grade_completion().
1615
     *
1616
     * @dataProvider get_grade_completion_provider
1617
     * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1618
     * @param bool $hasgrade Whether to set grade for the user in this activity.
1619
     * @param int|null $passinggrade Passing grade to set for the test activity.
1620
     * @param string|null $expectedexception Expected exception.
1621
     * @param int|null $expectedresult The expected completion status.
1622
     * @covers ::get_grade_completion
1623
     */
1624
    public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
11 efrain 1625
        ?string $expectedexception, ?int $expectedresult): void {
1 efrain 1626
        $this->setup_data();
1627
 
1628
        /** @var \mod_assign_generator $assigngenerator */
1629
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1630
        $assign = $assigngenerator->create_instance([
1631
            'course' => $this->course->id,
1632
            'completion' => COMPLETION_ENABLED,
1633
            'completionusegrade' => $completionusegrade,
1634
            'gradepass' => $passinggrade,
1635
        ]);
1636
 
1637
        $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1638
        if ($completionusegrade && $hasgrade) {
1639
            $assigninstance = new assign($cm->context, $cm, $this->course);
1640
            $grade = $assigninstance->get_user_grade($this->user->id, true);
1641
            $grade->grade = 75;
1642
            $assigninstance->update_grade($grade);
1643
        }
1644
 
1645
        $completioninfo = new completion_info($this->course);
1646
        if ($expectedexception) {
1647
            $this->expectException($expectedexception);
1648
        }
1649
        $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1650
        $this->assertEquals($expectedresult, $gradecompletion);
1651
    }
1652
 
1653
    /**
1654
     * Test the return value for cases when the activity module does not have associated grade_item.
1655
     *
1656
     * @covers ::get_grade_completion
1657
     */
11 efrain 1658
    public function test_get_grade_completion_without_grade_item(): void {
1 efrain 1659
        global $DB;
1660
 
1661
        $this->setup_data();
1662
 
1663
        $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1664
            'course' => $this->course->id,
1665
            'completion' => COMPLETION_ENABLED,
1666
            'completionusegrade' => true,
1667
            'gradepass' => 42,
1668
        ]);
1669
 
1670
        $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1671
 
1672
        $DB->delete_records('grade_items', [
1673
            'courseid' => $this->course->id,
1674
            'itemtype' => 'mod',
1675
            'itemmodule' => 'assign',
1676
            'iteminstance' => $assign->id,
1677
        ]);
1678
 
1679
        // Without the grade_item, the activity is considered incomplete.
1680
        $completioninfo = new completion_info($this->course);
1681
        $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1682
 
1683
        // Once the activity is graded, the grade_item is automatically created.
1684
        $assigninstance = new assign($cm->context, $cm, $this->course);
1685
        $grade = $assigninstance->get_user_grade($this->user->id, true);
1686
        $grade->grade = 40;
1687
        $assigninstance->update_grade($grade);
1688
 
1689
        // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1690
        $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1691
    }
1692
 
1693
    /**
1694
     * Test for aggregate_completions().
1695
     *
1696
     * @covers \aggregate_completions
1697
     */
11 efrain 1698
    public function test_aggregate_completions(): void {
1 efrain 1699
        global $DB, $CFG;
1700
        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
1701
        $this->resetAfterTest(true);
1702
        $time = time();
1703
 
1704
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1705
 
1706
        for ($i = 0; $i < 4; $i++) {
1707
            $students[] = $this->getDataGenerator()->create_user();
1708
        }
1709
 
1710
        $teacher = $this->getDataGenerator()->create_user();
1711
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1712
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1713
 
1714
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1715
        foreach ($students as $student) {
1716
            $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1717
        }
1718
 
1719
        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
1720
            array('completion' => 1));
1721
        $cmdata = get_coursemodule_from_id('data', $data->cmid);
1722
 
1723
        // Add activity completion criteria.
1724
        $criteriadata = new stdClass();
1725
        $criteriadata->id = $course->id;
1726
        $criteriadata->criteria_activity = array();
1727
        // Some activities.
1728
        $criteriadata->criteria_activity[$cmdata->id] = 1;
1729
        $class = 'completion_criteria_activity';
1730
        $criterion = new $class();
1731
        $criterion->update_config($criteriadata);
1732
 
1733
        $this->setUser($teacher);
1734
 
1735
        // Mark activity complete for both students.
1736
        $cm = get_coursemodule_from_instance('data', $data->id);
1737
        $completioncriteria = $DB->get_record('course_completion_criteria', []);
1738
        foreach ($students as $student) {
1739
            $cmcompletionrecords[] = (object)[
1740
                'coursemoduleid' => $cm->id,
1741
                'userid' => $student->id,
1742
                'completionstate' => 1,
1743
                'viewed' => 0,
1744
                'overrideby' => null,
1745
                'timemodified' => 0,
1746
            ];
1747
 
1748
            $usercompletions[] = (object)[
1749
                'criteriaid' => $completioncriteria->id,
1750
                'userid' => $student->id,
1751
                'timecompleted' => $time,
1752
            ];
1753
 
1754
            $cc = array(
1755
                'course'    => $course->id,
1756
                'userid'    => $student->id
1757
            );
1758
            $ccompletion = new completion_completion($cc);
1759
            $completion[] = $ccompletion->mark_inprogress($time);
1760
        }
1761
        $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1762
        $DB->insert_records('course_completion_crit_compl', $usercompletions);
1763
 
1764
        // MDL-33320: for instant completions we need aggregate to work in a single run.
1765
        $DB->set_field('course_completions', 'reaggregate', $time - 2);
1766
 
1767
        foreach ($students as $student) {
1768
            $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1769
            $this->assertFalse($result);
1770
        }
1771
 
1772
        aggregate_completions($completion[0]);
1773
 
1774
        $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]);
1775
        $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]);
1776
        $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]);
1777
 
1778
        $this->assertIsObject($result1);
1779
        $this->assertFalse($result2);
1780
        $this->assertFalse($result3);
1781
 
1782
        aggregate_completions(0);
1783
 
1784
        foreach ($students as $student) {
1785
            $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1786
            $this->assertIsObject($result);
1787
        }
1788
    }
1789
 
1790
    /**
1791
     * Test for completion_completion::_save().
1792
     *
1793
     * @covers \completion_completion::_save
1794
     */
11 efrain 1795
    public function test_save(): void {
1 efrain 1796
        global $DB;
1797
        $this->resetAfterTest(true);
1798
 
1799
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1800
 
1801
        $student = $this->getDataGenerator()->create_user();
1802
        $teacher = $this->getDataGenerator()->create_user();
1803
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1804
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1805
 
1806
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1807
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1808
 
1809
        $this->setUser($teacher);
1810
 
1811
        $cc = array(
1812
            'course'    => $course->id,
1813
            'userid'    => $student->id
1814
        );
1815
        $ccompletion = new completion_completion($cc);
1816
 
1817
        $completions = $DB->get_records('course_completions');
1818
        $this->assertEmpty($completions);
1819
 
1820
        // We're testing a private method, so we need to setup reflector magic.
1821
        $method = new ReflectionMethod($ccompletion, '_save');
1822
        $completionid = $method->invoke($ccompletion);
1823
        $completions = $DB->get_records('course_completions');
1824
        $this->assertEquals(count($completions), 1);
1825
        $this->assertEquals(reset($completions)->id, $completionid);
1826
 
1827
        $ccompletion->id = 0;
1828
        $method = new ReflectionMethod($ccompletion, '_save');
1829
        $completionid = $method->invoke($ccompletion);
1830
        $this->assertDebuggingCalled('Can not update data object, no id!');
1831
        $this->assertNull($completionid);
1832
    }
1833
 
1834
    /**
1835
     * Test for completion_completion::mark_enrolled().
1836
     *
1837
     * @covers \completion_completion::mark_enrolled
1838
     */
11 efrain 1839
    public function test_mark_enrolled(): void {
1 efrain 1840
        global $DB;
1841
        $this->resetAfterTest(true);
1842
 
1843
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1844
 
1845
        $student = $this->getDataGenerator()->create_user();
1846
        $teacher = $this->getDataGenerator()->create_user();
1847
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1848
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1849
 
1850
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1851
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1852
 
1853
        $this->setUser($teacher);
1854
 
1855
        $cc = array(
1856
            'course'    => $course->id,
1857
            'userid'    => $student->id
1858
        );
1859
        $ccompletion = new completion_completion($cc);
1860
 
1861
        $completions = $DB->get_records('course_completions');
1862
        $this->assertEmpty($completions);
1863
 
1864
        $completionid = $ccompletion->mark_enrolled();
1865
        $completions = $DB->get_records('course_completions');
1866
        $this->assertEquals(count($completions), 1);
1867
        $this->assertEquals(reset($completions)->id, $completionid);
1868
 
1869
        $ccompletion->id = 0;
1870
        $completionid = $ccompletion->mark_enrolled();
1871
        $this->assertDebuggingCalled('Can not update data object, no id!');
1872
        $this->assertNull($completionid);
1873
        $completions = $DB->get_records('course_completions');
1874
        $this->assertEquals(1, count($completions));
1875
    }
1876
 
1877
    /**
1878
     * Test for completion_completion::mark_inprogress().
1879
     *
1880
     * @covers \completion_completion::mark_inprogress
1881
     */
11 efrain 1882
    public function test_mark_inprogress(): void {
1 efrain 1883
        global $DB;
1884
        $this->resetAfterTest(true);
1885
 
1886
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1887
 
1888
        $student = $this->getDataGenerator()->create_user();
1889
        $teacher = $this->getDataGenerator()->create_user();
1890
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1891
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1892
 
1893
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1894
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1895
 
1896
        $this->setUser($teacher);
1897
 
1898
        $cc = array(
1899
            'course'    => $course->id,
1900
            'userid'    => $student->id
1901
        );
1902
        $ccompletion = new completion_completion($cc);
1903
 
1904
        $completions = $DB->get_records('course_completions');
1905
        $this->assertEmpty($completions);
1906
 
1907
        $completionid = $ccompletion->mark_inprogress();
1908
        $completions = $DB->get_records('course_completions');
1909
        $this->assertEquals(1, count($completions));
1910
        $this->assertEquals(reset($completions)->id, $completionid);
1911
 
1912
        $ccompletion->id = 0;
1913
        $completionid = $ccompletion->mark_inprogress();
1914
        $this->assertDebuggingCalled('Can not update data object, no id!');
1915
        $this->assertNull($completionid);
1916
        $completions = $DB->get_records('course_completions');
1917
        $this->assertEquals(1, count($completions));
1918
    }
1919
 
1920
    /**
1921
     * Test for completion_completion::mark_complete().
1922
     *
1923
     * @covers \completion_completion::mark_complete
1924
     */
11 efrain 1925
    public function test_mark_complete(): void {
1 efrain 1926
        global $DB;
1927
        $this->resetAfterTest(true);
1928
 
1929
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1930
 
1931
        $student = $this->getDataGenerator()->create_user();
1932
        $teacher = $this->getDataGenerator()->create_user();
1933
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1934
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1935
 
1936
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1937
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1938
 
1939
        $this->setUser($teacher);
1940
 
1941
        $cc = array(
1942
            'course'    => $course->id,
1943
            'userid'    => $student->id
1944
        );
1945
        $ccompletion = new completion_completion($cc);
1946
 
1947
        $completions = $DB->get_records('course_completions');
1948
        $this->assertEmpty($completions);
1949
 
1950
        $completionid = $ccompletion->mark_complete();
1951
        $completions = $DB->get_records('course_completions');
1952
        $this->assertEquals(1, count($completions));
1953
        $this->assertEquals(reset($completions)->id, $completionid);
1954
 
1955
        $ccompletion->id = 0;
1956
        $completionid = $ccompletion->mark_complete();
1957
        $this->assertNull($completionid);
1958
        $completions = $DB->get_records('course_completions');
1959
        $this->assertEquals(1, count($completions));
1960
    }
1961
 
1962
    /**
1963
     * Test for completion_criteria_completion::mark_complete().
1964
     *
1965
     * @covers \completion_criteria_completion::mark_complete
1966
     */
11 efrain 1967
    public function test_criteria_mark_complete(): void {
1 efrain 1968
        global $DB;
1969
        $this->resetAfterTest(true);
1970
 
1971
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1972
 
1973
        $student = $this->getDataGenerator()->create_user();
1974
        $teacher = $this->getDataGenerator()->create_user();
1975
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1976
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1977
 
1978
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1979
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1980
 
1981
        $this->setUser($teacher);
1982
 
1983
        $record = [
1984
            'course'    => $course->id,
1985
            'criteriaid'    => 1,
1986
            'userid'    => $student->id,
1987
            'timecompleted' => time()
1988
        ];
1989
        $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
1990
 
1991
        $completions = $DB->get_records('course_completions');
1992
        $this->assertEmpty($completions);
1993
 
1994
        $completionid = $completion->mark_complete($record['timecompleted']);
1995
        $completions = $DB->get_records('course_completions');
1996
        $this->assertEquals(1, count($completions));
1997
        $this->assertEquals(reset($completions)->id, $completionid);
1998
    }
1999
 
2000
    /**
2001
     * Test that data is cleaned when we reset a course completion data
2002
     *
2003
     * @covers ::delete_all_completion_data
2004
     */
11 efrain 2005
    public function test_course_reset_completion(): void {
1 efrain 2006
        global $DB;
2007
 
2008
        $this->setup_data();
2009
 
2010
        $page = $this->getDataGenerator()->create_module('page', [
2011
            'course' => $this->course->id,
2012
            'completion' => COMPLETION_ENABLED,
2013
            'completionview' => COMPLETION_VIEW_REQUIRED,
2014
        ]);
2015
        $cm = cm_info::create(get_coursemodule_from_instance('page', $page->id));
2016
        $completion = new completion_info($this->course);
2017
        $completion->set_module_viewed($cm, $this->user->id);
2018
        // Sanity test.
2019
        $this->assertTrue($DB->record_exists_select('course_modules_completion',
2020
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2021
            ['course' => $this->course->id]
2022
        ));
2023
        $this->assertTrue($DB->record_exists_select('course_modules_viewed',
2024
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2025
            ['course' => $this->course->id]
2026
        ));
2027
        // Deleting the prerequisite course should remove the completion criteria.
2028
        $resetdata = new \stdClass();
2029
        $resetdata->id = $this->course->id;
2030
        $resetdata->reset_completion = true;
2031
        reset_course_userdata($resetdata);
2032
 
2033
        $this->assertFalse($DB->record_exists_select('course_modules_completion',
2034
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2035
            ['course' => $this->course->id]
2036
        ));
2037
        $this->assertFalse($DB->record_exists_select('course_modules_viewed',
2038
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2039
            ['course' => $this->course->id]
2040
        ));
2041
    }
2042
}
2043
 
2044
class core_completionlib_fake_recordset implements Iterator {
2045
    protected $closed;
2046
    protected $values, $index;
2047
 
2048
    public function __construct($values) {
2049
        $this->values = $values;
2050
        $this->index = 0;
2051
    }
2052
 
2053
    #[\ReturnTypeWillChange]
2054
    public function current() {
2055
        return $this->values[$this->index];
2056
    }
2057
 
2058
    #[\ReturnTypeWillChange]
2059
    public function key() {
2060
        return $this->values[$this->index];
2061
    }
2062
 
2063
    public function next(): void {
2064
        $this->index++;
2065
    }
2066
 
2067
    public function rewind(): void {
2068
        $this->index = 0;
2069
    }
2070
 
2071
    public function valid(): bool {
2072
        return count($this->values) > $this->index;
2073
    }
2074
 
2075
    public function close() {
2076
        $this->closed = true;
2077
    }
2078
 
2079
    public function was_closed() {
2080
        return $this->closed;
2081
    }
2082
}