MDL-67613 availability_completion: replacing old arrays
[moodle.git] / availability / condition / completion / tests / condition_test.php
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/>.
17 /**
18  * Unit tests for the completion condition.
19  *
20  * @package availability_completion
21  * @copyright 2014 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 use availability_completion\condition;
29 global $CFG;
30 require_once($CFG->libdir . '/completionlib.php');
32 /**
33  * Unit tests for the completion condition.
34  *
35  * @package availability_completion
36  * @copyright 2014 The Open University
37  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class availability_completion_condition_testcase extends advanced_testcase {
41     /**
42      * Setup to ensure that fixtures are loaded.
43      */
44     public static function setupBeforeClass(): void {
45         global $CFG;
46         // Load the mock info class so that it can be used.
47         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
48         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php');
49         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php');
50     }
52     /**
53      * Load required classes.
54      */
55     public function setUp() {
56         availability_completion\condition::wipe_static_cache();
57     }
59     /**
60      * Tests constructing and using condition as part of tree.
61      */
62     public function test_in_tree() {
63         global $USER, $CFG;
64         $this->resetAfterTest();
66         $this->setAdminUser();
68         // Create course with completion turned on and a Page.
69         $CFG->enablecompletion = true;
70         $CFG->enableavailability = true;
71         $generator = $this->getDataGenerator();
72         $course = $generator->create_course(['enablecompletion' => 1]);
73         $page = $generator->get_plugin_generator('mod_page')->create_instance(
74                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
75         $selfpage = $generator->get_plugin_generator('mod_page')->create_instance(
76                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
78         $modinfo = get_fast_modinfo($course);
79         $cm = $modinfo->get_cm($page->cmid);
80         $info = new \core_availability\mock_info($course, $USER->id);
82         $structure = (object)[
83             'op' => '|',
84             'show' => true,
85             'c' => [
86                 (object)[
87                     'type' => 'completion',
88                     'cm' => (int)$cm->id,
89                     'e' => COMPLETION_COMPLETE
90                 ]
91             ]
92         ];
93         $tree = new \core_availability\tree($structure);
95         // Initial check (user has not completed activity).
96         $result = $tree->check_available(false, $info, true, $USER->id);
97         $this->assertFalse($result->is_available());
99         // Mark activity complete.
100         $completion = new completion_info($course);
101         $completion->update_state($cm, COMPLETION_COMPLETE);
103         // Now it's true!
104         $result = $tree->check_available(false, $info, true, $USER->id);
105         $this->assertTrue($result->is_available());
106     }
108     /**
109      * Tests the constructor including error conditions. Also tests the
110      * string conversion feature (intended for debugging only).
111      */
112     public function test_constructor() {
113         // No parameters.
114         $structure = new stdClass();
115         try {
116             $cond = new condition($structure);
117             $this->fail();
118         } catch (coding_exception $e) {
119             $this->assertContains('Missing or invalid ->cm', $e->getMessage());
120         }
122         // Invalid $cm.
123         $structure->cm = 'hello';
124         try {
125             $cond = new condition($structure);
126             $this->fail();
127         } catch (coding_exception $e) {
128             $this->assertContains('Missing or invalid ->cm', $e->getMessage());
129         }
131         // Missing $e.
132         $structure->cm = 42;
133         try {
134             $cond = new condition($structure);
135             $this->fail();
136         } catch (coding_exception $e) {
137             $this->assertContains('Missing or invalid ->e', $e->getMessage());
138         }
140         // Invalid $e.
141         $structure->e = 99;
142         try {
143             $cond = new condition($structure);
144             $this->fail();
145         } catch (coding_exception $e) {
146             $this->assertContains('Missing or invalid ->e', $e->getMessage());
147         }
149         // Successful construct & display with all different expected values.
150         $structure->e = COMPLETION_COMPLETE;
151         $cond = new condition($structure);
152         $this->assertEquals('{completion:cm42 COMPLETE}', (string)$cond);
154         $structure->e = COMPLETION_COMPLETE_PASS;
155         $cond = new condition($structure);
156         $this->assertEquals('{completion:cm42 COMPLETE_PASS}', (string)$cond);
158         $structure->e = COMPLETION_COMPLETE_FAIL;
159         $cond = new condition($structure);
160         $this->assertEquals('{completion:cm42 COMPLETE_FAIL}', (string)$cond);
162         $structure->e = COMPLETION_INCOMPLETE;
163         $cond = new condition($structure);
164         $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
166         // Successful contruct with previous activity.
167         $structure->cm = condition::OPTION_PREVIOUS;
168         $cond = new condition($structure);
169         $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond);
171     }
173     /**
174      * Tests the save() function.
175      */
176     public function test_save() {
177         $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE];
178         $cond = new condition($structure);
179         $structure->type = 'completion';
180         $this->assertEquals($structure, $cond->save());
181     }
183     /**
184      * Tests the is_available and get_description functions.
185      */
186     public function test_usage() {
187         global $CFG, $DB;
188         require_once($CFG->dirroot . '/mod/assign/locallib.php');
189         $this->resetAfterTest();
191         // Create course with completion turned on.
192         $CFG->enablecompletion = true;
193         $CFG->enableavailability = true;
194         $generator = $this->getDataGenerator();
195         $course = $generator->create_course(['enablecompletion' => 1]);
196         $user = $generator->create_user();
197         $generator->enrol_user($user->id, $course->id);
198         $this->setUser($user);
200         // Create a Page with manual completion for basic checks.
201         $page = $generator->get_plugin_generator('mod_page')->create_instance(
202                 ['course' => $course->id, 'name' => 'Page!',
203                 'completion' => COMPLETION_TRACKING_MANUAL]);
205         // Create an assignment - we need to have something that can be graded
206         // so as to test the PASS/FAIL states. Set it up to be completed based
207         // on its grade item.
208         $assignrow = $this->getDataGenerator()->create_module('assign', [
209                         'course' => $course->id, 'name' => 'Assign!',
210                         'completion' => COMPLETION_TRACKING_AUTOMATIC]);
211         $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
212                 ['id' => $assignrow->cmid]);
213         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
215         // Get basic details.
216         $modinfo = get_fast_modinfo($course);
217         $pagecm = $modinfo->get_cm($page->cmid);
218         $assigncm = $assign->get_course_module();
219         $info = new \core_availability\mock_info($course, $user->id);
221         // COMPLETE state (false), positive and NOT.
222         $cond = new condition((object)[
223             'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
224         ]);
225         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
226         $information = $cond->get_description(false, false, $info);
227         $information = \core_availability\info::format_info($information, $course);
228         $this->assertRegExp('~Page!.*is marked complete~', $information);
229         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
231         // INCOMPLETE state (true).
232         $cond = new condition((object)[
233             'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
234         ]);
235         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
236         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
237         $information = $cond->get_description(false, true, $info);
238         $information = \core_availability\info::format_info($information, $course);
239         $this->assertRegExp('~Page!.*is marked complete~', $information);
241         // Mark page complete.
242         $completion = new completion_info($course);
243         $completion->update_state($pagecm, COMPLETION_COMPLETE);
245         // COMPLETE state (true).
246         $cond = new condition((object)[
247             'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
248         ]);
249         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
250         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
251         $information = $cond->get_description(false, true, $info);
252         $information = \core_availability\info::format_info($information, $course);
253         $this->assertRegExp('~Page!.*is incomplete~', $information);
255         // INCOMPLETE state (false).
256         $cond = new condition((object)[
257             'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
258         ]);
259         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
260         $information = $cond->get_description(false, false, $info);
261         $information = \core_availability\info::format_info($information, $course);
262         $this->assertRegExp('~Page!.*is incomplete~', $information);
263         $this->assertTrue($cond->is_available(true, $info,
264                 true, $user->id));
266         // We are going to need the grade item so that we can get pass/fails.
267         $gradeitem = $assign->get_grade_item();
268         grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
269         $gradeitem->update();
271         // With no grade, it should return true for INCOMPLETE and false for
272         // the other three.
273         $cond = new condition((object)[
274             'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
275         ]);
276         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
277         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
279         $cond = new condition((object)[
280             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
281         ]);
282         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
283         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
285         // Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
286         $cond = new condition((object)[
287             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
288         ]);
289         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
290         $information = $cond->get_description(false, false, $info);
291         $information = \core_availability\info::format_info($information, $course);
292         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
293         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
295         $cond = new condition((object)[
296             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
297         ]);
298         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
299         $information = $cond->get_description(false, false, $info);
300         $information = \core_availability\info::format_info($information, $course);
301         $this->assertRegExp('~Assign!.*is complete and failed~', $information);
302         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
304         // Change the grade to be complete and failed.
305         self::set_grade($assignrow, $user->id, 40);
307         $cond = new condition((object)[
308             'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
309         ]);
310         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
311         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
313         $cond = new condition((object)[
314             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
315         ]);
316         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
317         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
319         $cond = new condition((object)[
320             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
321         ]);
322         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
323         $information = $cond->get_description(false, false, $info);
324         $information = \core_availability\info::format_info($information, $course);
325         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
326         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
328         $cond = new condition((object)[
329             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
330         ]);
331         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
332         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
333         $information = $cond->get_description(false, true, $info);
334         $information = \core_availability\info::format_info($information, $course);
335         $this->assertRegExp('~Assign!.*is not complete and failed~', $information);
337         // Now change it to pass.
338         self::set_grade($assignrow, $user->id, 60);
340         $cond = new condition((object)[
341             'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
342         ]);
343         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
344         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
346         $cond = new condition((object)[
347             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
348         ]);
349         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
350         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
352         $cond = new condition((object)[
353                         'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
354                     ]);
355         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
356         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
357         $information = $cond->get_description(false, true, $info);
358         $information = \core_availability\info::format_info($information, $course);
359         $this->assertRegExp('~Assign!.*is not complete and passed~', $information);
361         $cond = new condition((object)[
362             'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
363         ]);
364         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
365         $information = $cond->get_description(false, false, $info);
366         $information = \core_availability\info::format_info($information, $course);
367         $this->assertRegExp('~Assign!.*is complete and failed~', $information);
368         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
370         // Simulate deletion of an activity by using an invalid cmid. These
371         // conditions always fail, regardless of NOT flag or INCOMPLETE.
372         $cond = new condition((object)[
373             'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE
374         ]);
375         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
376         $information = $cond->get_description(false, false, $info);
377         $information = \core_availability\info::format_info($information, $course);
378         $this->assertRegExp('~(Missing activity).*is marked complete~', $information);
379         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
380         $cond = new condition((object)[
381             'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE
382         ]);
383         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
384     }
386     /**
387      * Tests the is_available and get_description functions for previous activity option.
388      *
389      * @dataProvider test_previous_activity_data
390      * @param int $grade the current assign grade (0 for none)
391      * @param int $condition true for complete, false for incomplete
392      * @param string $mark activity to mark as complete
393      * @param string $activity activity name to test
394      * @param bool $result if it must be available or not
395      * @param bool $resultnot if it must be available when the condition is inverted
396      * @param string $description the availabiklity text to check
397      */
398     public function test_previous_activity(int $grade, int $condition, string $mark, string $activity,
399             bool $result, bool $resultnot, string $description): void {
400         global $CFG, $DB;
401         require_once($CFG->dirroot . '/mod/assign/locallib.php');
402         $this->resetAfterTest();
404         // Create course with completion turned on.
405         $CFG->enablecompletion = true;
406         $CFG->enableavailability = true;
407         $generator = $this->getDataGenerator();
408         $course = $generator->create_course(['enablecompletion' => 1]);
409         $user = $generator->create_user();
410         $generator->enrol_user($user->id, $course->id);
411         $this->setUser($user);
413         // Page 1 (manual completion).
414         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
415                 ['course' => $course->id, 'name' => 'Page1!',
416                 'completion' => COMPLETION_TRACKING_MANUAL]);
418         // Page 2 (manual completion).
419         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
420                 ['course' => $course->id, 'name' => 'Page2!',
421                 'completion' => COMPLETION_TRACKING_MANUAL]);
423         // Page ignored (no completion).
424         $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance(
425                 ['course' => $course->id, 'name' => 'Page ignored!']);
427         // Create an assignment - we need to have something that can be graded
428         // so as to test the PASS/FAIL states. Set it up to be completed based
429         // on its grade item.
430         $assignrow = $this->getDataGenerator()->create_module('assign', [
431             'course' => $course->id, 'name' => 'Assign!',
432             'completion' => COMPLETION_TRACKING_AUTOMATIC
433         ]);
434         $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
435                 ['id' => $assignrow->cmid]);
436         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
438         // Page 3 (manual completion).
439         $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
440                 ['course' => $course->id, 'name' => 'Page3!',
441                 'completion' => COMPLETION_TRACKING_MANUAL]);
443         // Get basic details.
444         $activities = [];
445         $modinfo = get_fast_modinfo($course);
446         $activities['page1'] = $modinfo->get_cm($page1->cmid);
447         $activities['page2'] = $modinfo->get_cm($page2->cmid);
448         $activities['assign'] = $assign->get_course_module();
449         $activities['page3'] = $modinfo->get_cm($page3->cmid);
450         $prevvalue = condition::OPTION_PREVIOUS;
452         // Setup gradings and completion.
453         if ($grade) {
454             $gradeitem = $assign->get_grade_item();
455             grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
456             $gradeitem->update();
457             self::set_grade($assignrow, $user->id, $grade);
458         }
459         if ($mark) {
460             $completion = new completion_info($course);
461             $completion->update_state($activities[$mark], COMPLETION_COMPLETE);
462         }
464         // Set opprevious WITH non existent previous activity.
465         $info = new \core_availability\mock_info_module($user->id, $activities[$activity]);
466         $cond = new condition((object)[
467             'cm' => (int)$prevvalue, 'e' => $condition
468         ]);
470         // Do the checks.
471         $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
472         $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
473         $information = $cond->get_description(false, false, $info);
474         $information = \core_availability\info::format_info($information, $course);
475         $this->assertRegExp($description, $information);
476     }
478     public function test_previous_activity_data(): array {
479         // Assign grade, condition, activity to complete, activity to test, result, resultnot, description.
480         return [
481             'Missing previous activity complete' => [
482                 0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~'
483             ],
484             'Missing previous activity incomplete' => [
485                 0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~'
486             ],
487             'Previous complete condition with previous activity incompleted' => [
488                 0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~'
489             ],
490             'Previous incomplete condition with previous activity incompleted' => [
491                 0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~'
492             ],
493             'Previous complete condition with previous activity completed' => [
494                 0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~'
495             ],
496             'Previous incomplete condition with previous activity completed' => [
497                 0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~'
498             ],
499             // Depenging on page pass fail (pages are not gradable).
500             'Previous complete pass condition with previous no gradable activity incompleted' => [
501                 0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~'
502             ],
503             'Previous complete fail condition with previous no gradable activity incompleted' => [
504                 0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~'
505             ],
506             'Previous complete pass condition with previous no gradable activity completed' => [
507                 0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~'
508             ],
509             'Previous complete fail condition with previous no gradable activity completed' => [
510                 0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~'
511             ],
512             // There's an page without completion between page2 ans assign.
513             'Previous complete condition with sibling activity incompleted' => [
514                 0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~'
515             ],
516             'Previous incomplete condition with sibling activity incompleted' => [
517                 0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~'
518             ],
519             'Previous complete condition with sibling activity completed' => [
520                 0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~'
521             ],
522             'Previous incomplete condition with sibling activity completed' => [
523                 0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~'
524             ],
525             // Depending on assign without grade.
526             'Previous complete condition with previous without grade' => [
527                 0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~'
528             ],
529             'Previous incomplete condition with previous without grade' => [
530                 0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~'
531             ],
532             'Previous complete pass condition with previous without grade' => [
533                 0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
534             ],
535             'Previous complete fail condition with previous without grade' => [
536                 0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
537             ],
538             // Depending on assign with grade.
539             'Previous complete condition with previous fail grade' => [
540                 40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
541             ],
542             'Previous incomplete condition with previous fail grade' => [
543                 40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
544             ],
545             'Previous complete pass condition with previous fail grade' => [
546                 40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
547             ],
548             'Previous complete fail condition with previous fail grade' => [
549                 40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~'
550             ],
551             'Previous complete condition with previous pass grade' => [
552                 60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
553             ],
554             'Previous incomplete condition with previous pass grade' => [
555                 60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
556             ],
557             'Previous complete pass condition with previous pass grade' => [
558                 60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~'
559             ],
560             'Previous complete fail condition with previous pass grade' => [
561                 60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
562             ],
563         ];
564     }
566     /**
567      * Tests the is_available and get_description functions for
568      * previous activity option in course sections.
569      *
570      * @dataProvider test_section_previous_activity_data
571      * @param int $condition condition value
572      * @param bool $mark if Page 1 must be mark as completed
573      * @param string $section section to add the availability
574      * @param bool $result expected result
575      * @param bool $resultnot expected negated result
576      * @param string $description description to match
577      */
578     public function test_section_previous_activity(int $condition, bool $mark, string $section,
579                 bool $result, bool $resultnot, string $description): void {
580         global $CFG, $DB;
581         require_once($CFG->dirroot . '/mod/assign/locallib.php');
582         $this->resetAfterTest();
584         // Create course with completion turned on.
585         $CFG->enablecompletion = true;
586         $CFG->enableavailability = true;
587         $generator = $this->getDataGenerator();
588         $course = $generator->create_course(
589                 ['numsections' => 4, 'enablecompletion' => 1],
590                 ['createsections' => true]);
591         $user = $generator->create_user();
592         $generator->enrol_user($user->id, $course->id);
593         $this->setUser($user);
595         // Section 1 - page1 (manual completion).
596         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
597                 ['course' => $course->id, 'name' => 'Page1!', 'section' => 1,
598                 'completion' => COMPLETION_TRACKING_MANUAL]);
600         // Section 1 - page ignored 1 (no completion).
601         $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance(
602                 ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]);
604         // Section 2 - page ignored 2 (no completion).
605         $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance(
606                 ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]);
608         // Section 3 - page2 (manual completion).
609         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
610                 ['course' => $course->id, 'name' => 'Page2!', 'section' => 3,
611                 'completion' => COMPLETION_TRACKING_MANUAL]);
613         // Section 4 is empty.
615         // Get basic details.
616         get_fast_modinfo(0, 0, true);
617         $modinfo = get_fast_modinfo($course);
618         $sections['section1'] = $modinfo->get_section_info(1);
619         $sections['section2'] = $modinfo->get_section_info(2);
620         $sections['section3'] = $modinfo->get_section_info(3);
621         $sections['section4'] = $modinfo->get_section_info(4);
622         $page1cm = $modinfo->get_cm($page1->cmid);
623         $prevvalue = condition::OPTION_PREVIOUS;
625         if ($mark) {
626             // Mark page1 complete.
627             $completion = new completion_info($course);
628             $completion->update_state($page1cm, COMPLETION_COMPLETE);
629         }
631         $info = new \core_availability\mock_info_section($user->id, $sections[$section]);
632         $cond = new condition((object)[
633             'cm' => (int)$prevvalue, 'e' => $condition
634         ]);
635         $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
636         $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
637         $information = $cond->get_description(false, false, $info);
638         $information = \core_availability\info::format_info($information, $course);
639         $this->assertRegExp($description, $information);
641     }
643     public function test_section_previous_activity_data(): array {
644         return [
645             // Condition, Activity completion, section to test, result, resultnot, description.
646             'Completion complete Section with no previous activity' => [
647                 COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~'
648             ],
649             'Completion incomplete Section with no previous activity' => [
650                 COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~'
651             ],
652             // Section 2 depending on section 1 -> Page 1 (no grading).
653             'Completion complete Section with previous activity incompleted' => [
654                 COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~'
655             ],
656             'Completion incomplete Section with previous activity incompleted' => [
657                 COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~'
658             ],
659             'Completion complete Section with previous activity completed' => [
660                 COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~'
661             ],
662             'Completion incomplete Section with previous activity completed' => [
663                 COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~'
664             ],
665             // Section 3 depending on section 1 -> Page 1 (no grading).
666             'Completion complete Section ignoring empty sections and activity incompleted' => [
667                 COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~'
668             ],
669             'Completion incomplete Section ignoring empty sections and activity incompleted' => [
670                 COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~'
671             ],
672             'Completion complete Section ignoring empty sections and activity completed' => [
673                 COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~'
674             ],
675             'Completion incomplete Section ignoring empty sections and activity completed' => [
676                 COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~'
677             ],
678             // Section 4 depending on section 3 -> Page 2 (no grading).
679             'Completion complete Last section with previous activity incompleted' => [
680                 COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~'
681             ],
682             'Completion incomplete Last section with previous activity incompleted' => [
683                 COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~'
684             ],
685             'Completion complete Last section with previous activity completed' => [
686                 COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~'
687             ],
688             'Completion incomplete Last section with previous activity completed' => [
689                 COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~'
690             ],
691         ];
692     }
694     /**
695      * Tests completion_value_used static function.
696      */
697     public function test_completion_value_used() {
698         global $CFG, $DB;
699         $this->resetAfterTest();
700         $prevvalue = condition::OPTION_PREVIOUS;
702         // Create course with completion turned on and some sections.
703         $CFG->enablecompletion = true;
704         $CFG->enableavailability = true;
705         $generator = $this->getDataGenerator();
706         $course = $generator->create_course(
707                 ['numsections' => 1, 'enablecompletion' => 1],
708                 ['createsections' => true]);
710         // Create six pages with manual completion.
711         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
712                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
713         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
714                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
715         $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
716                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
717         $page4 = $generator->get_plugin_generator('mod_page')->create_instance(
718                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
719         $page5 = $generator->get_plugin_generator('mod_page')->create_instance(
720                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
721         $page6 = $generator->get_plugin_generator('mod_page')->create_instance(
722                 ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
724         // Set up page3 to depend on page1, and section1 to depend on page2.
725         $DB->set_field('course_modules', 'availability',
726                 '{"op":"|","show":true,"c":[' .
727                 '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
728                 ['id' => $page3->cmid]);
729         $DB->set_field('course_sections', 'availability',
730                 '{"op":"|","show":true,"c":[' .
731                 '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
732                 ['course' => $course->id, 'section' => 1]);
733         // Set up page5 and page6 to depend on previous activity.
734         $DB->set_field('course_modules', 'availability',
735                 '{"op":"|","show":true,"c":[' .
736                 '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
737                 ['id' => $page5->cmid]);
738         $DB->set_field('course_modules', 'availability',
739                 '{"op":"|","show":true,"c":[' .
740                 '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
741                 ['id' => $page6->cmid]);
743         // Check 1: nothing depends on page3 and page6 but something does on the others.
744         $this->assertTrue(availability_completion\condition::completion_value_used(
745                 $course, $page1->cmid));
746         $this->assertTrue(availability_completion\condition::completion_value_used(
747                 $course, $page2->cmid));
748         $this->assertFalse(availability_completion\condition::completion_value_used(
749                 $course, $page3->cmid));
750         $this->assertTrue(availability_completion\condition::completion_value_used(
751                 $course, $page4->cmid));
752         $this->assertTrue(availability_completion\condition::completion_value_used(
753                 $course, $page5->cmid));
754         $this->assertFalse(availability_completion\condition::completion_value_used(
755                 $course, $page6->cmid));
756     }
758     /**
759      * Updates the grade of a user in the given assign module instance.
760      *
761      * @param stdClass $assignrow Assignment row from database
762      * @param int $userid User id
763      * @param float $grade Grade
764      */
765     protected static function set_grade($assignrow, $userid, $grade) {
766         $grades = [];
767         $grades[$userid] = (object)[
768             'rawgrade' => $grade, 'userid' => $userid
769         ];
770         $assignrow->cmidnumber = null;
771         assign_grade_item_update($assignrow, $grades);
772     }
774     /**
775      * Tests the update_dependency_id() function.
776      */
777     public function test_update_dependency_id() {
778         $cond = new condition((object)[
779             'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43
780         ]);
781         $this->assertFalse($cond->update_dependency_id('frogs', 42, 540));
782         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
783         $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456));
784         $after = $cond->save();
785         $this->assertEquals(456, $after->cm);
787         // Test selfid updating.
788         $cond = new condition((object)[
789             'cm' => 42, 'e' => COMPLETION_COMPLETE
790         ]);
791         $this->assertFalse($cond->update_dependency_id('frogs', 43, 540));
792         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
793         $after = $cond->save();
794         $this->assertEquals(42, $after->cm);
796         // Test on previous activity.
797         $cond = new condition((object)[
798             'cm' => condition::OPTION_PREVIOUS,
799             'e' => COMPLETION_COMPLETE
800         ]);
801         $this->assertFalse($cond->update_dependency_id('frogs', 43, 80));
802         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
803         $after = $cond->save();
804         $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm);
805     }