MDL-68430 filter_mathjaxloader: update default CDN to 2.7.8
[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 {
40     /**
41      * Load required classes.
42      */
43     public function setUp() {
44         // Load the mock info class so that it can be used.
45         global $CFG;
46         require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
47     }
49     /**
50      * Tests constructing and using condition as part of tree.
51      */
52     public function test_in_tree() {
53         global $USER, $CFG;
54         $this->resetAfterTest();
56         $this->setAdminUser();
58         // Create course with completion turned on and a Page.
59         $CFG->enablecompletion = true;
60         $CFG->enableavailability = true;
61         $generator = $this->getDataGenerator();
62         $course = $generator->create_course(array('enablecompletion' => 1));
63         $page = $generator->get_plugin_generator('mod_page')->create_instance(
64                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
66         $modinfo = get_fast_modinfo($course);
67         $cm = $modinfo->get_cm($page->cmid);
68         $info = new \core_availability\mock_info($course, $USER->id);
70         $structure = (object)array('op' => '|', 'show' => true, 'c' => array(
71                 (object)array('type' => 'completion', 'cm' => (int)$cm->id,
72                 'e' => COMPLETION_COMPLETE)));
73         $tree = new \core_availability\tree($structure);
75         // Initial check (user has not completed activity).
76         $result = $tree->check_available(false, $info, true, $USER->id);
77         $this->assertFalse($result->is_available());
79         // Mark activity complete.
80         $completion = new completion_info($course);
81         $completion->update_state($cm, COMPLETION_COMPLETE);
83         // Now it's true!
84         $result = $tree->check_available(false, $info, true, $USER->id);
85         $this->assertTrue($result->is_available());
86     }
88     /**
89      * Tests the constructor including error conditions. Also tests the
90      * string conversion feature (intended for debugging only).
91      */
92     public function test_constructor() {
93         // No parameters.
94         $structure = new stdClass();
95         try {
96             $cond = new condition($structure);
97             $this->fail();
98         } catch (coding_exception $e) {
99             $this->assertContains('Missing or invalid ->cm', $e->getMessage());
100         }
102         // Invalid $cm.
103         $structure->cm = 'hello';
104         try {
105             $cond = new condition($structure);
106             $this->fail();
107         } catch (coding_exception $e) {
108             $this->assertContains('Missing or invalid ->cm', $e->getMessage());
109         }
111         // Missing $e.
112         $structure->cm = 42;
113         try {
114             $cond = new condition($structure);
115             $this->fail();
116         } catch (coding_exception $e) {
117             $this->assertContains('Missing or invalid ->e', $e->getMessage());
118         }
120         // Invalid $e.
121         $structure->e = 99;
122         try {
123             $cond = new condition($structure);
124             $this->fail();
125         } catch (coding_exception $e) {
126             $this->assertContains('Missing or invalid ->e', $e->getMessage());
127         }
129         // Successful construct & display with all different expected values.
130         $structure->e = COMPLETION_COMPLETE;
131         $cond = new condition($structure);
132         $this->assertEquals('{completion:cm42 COMPLETE}', (string)$cond);
134         $structure->e = COMPLETION_COMPLETE_PASS;
135         $cond = new condition($structure);
136         $this->assertEquals('{completion:cm42 COMPLETE_PASS}', (string)$cond);
138         $structure->e = COMPLETION_COMPLETE_FAIL;
139         $cond = new condition($structure);
140         $this->assertEquals('{completion:cm42 COMPLETE_FAIL}', (string)$cond);
142         $structure->e = COMPLETION_INCOMPLETE;
143         $cond = new condition($structure);
144         $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
145     }
147     /**
148      * Tests the save() function.
149      */
150     public function test_save() {
151         $structure = (object)array('cm' => 42, 'e' => COMPLETION_COMPLETE);
152         $cond = new condition($structure);
153         $structure->type = 'completion';
154         $this->assertEquals($structure, $cond->save());
155     }
157     /**
158      * Tests the is_available and get_description functions.
159      */
160     public function test_usage() {
161         global $CFG, $DB;
162         require_once($CFG->dirroot . '/mod/assign/locallib.php');
163         $this->resetAfterTest();
165         // Create course with completion turned on.
166         $CFG->enablecompletion = true;
167         $CFG->enableavailability = true;
168         $generator = $this->getDataGenerator();
169         $course = $generator->create_course(array('enablecompletion' => 1));
170         $user = $generator->create_user();
171         $generator->enrol_user($user->id, $course->id);
172         $this->setUser($user);
174         // Create a Page with manual completion for basic checks.
175         $page = $generator->get_plugin_generator('mod_page')->create_instance(
176                 array('course' => $course->id, 'name' => 'Page!',
177                 'completion' => COMPLETION_TRACKING_MANUAL));
179         // Create an assignment - we need to have something that can be graded
180         // so as to test the PASS/FAIL states. Set it up to be completed based
181         // on its grade item.
182         $assignrow = $this->getDataGenerator()->create_module('assign', array(
183                 'course' => $course->id, 'name' => 'Assign!',
184                 'completion' => COMPLETION_TRACKING_AUTOMATIC));
185         $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
186                 array('id' => $assignrow->cmid));
187         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
189         // Get basic details.
190         $modinfo = get_fast_modinfo($course);
191         $pagecm = $modinfo->get_cm($page->cmid);
192         $assigncm = $assign->get_course_module();
193         $info = new \core_availability\mock_info($course, $user->id);
195         // COMPLETE state (false), positive and NOT.
196         $cond = new condition((object)array(
197                 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
198         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
199         $information = $cond->get_description(false, false, $info);
200         $information = \core_availability\info::format_info($information, $course);
201         $this->assertRegExp('~Page!.*is marked complete~', $information);
202         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
204         // INCOMPLETE state (true).
205         $cond = new condition((object)array(
206                 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
207         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
208         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
209         $information = $cond->get_description(false, true, $info);
210         $information = \core_availability\info::format_info($information, $course);
211         $this->assertRegExp('~Page!.*is marked complete~', $information);
213         // Mark page complete.
214         $completion = new completion_info($course);
215         $completion->update_state($pagecm, COMPLETION_COMPLETE);
217         // COMPLETE state (true).
218         $cond = new condition((object)array(
219                 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
220         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
221         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
222         $information = $cond->get_description(false, true, $info);
223         $information = \core_availability\info::format_info($information, $course);
224         $this->assertRegExp('~Page!.*is incomplete~', $information);
226         // INCOMPLETE state (false).
227         $cond = new condition((object)array(
228                 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
229         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
230         $information = $cond->get_description(false, false, $info);
231         $information = \core_availability\info::format_info($information, $course);
232         $this->assertRegExp('~Page!.*is incomplete~', $information);
233         $this->assertTrue($cond->is_available(true, $info,
234                 true, $user->id));
236         // We are going to need the grade item so that we can get pass/fails.
237         $gradeitem = $assign->get_grade_item();
238         grade_object::set_properties($gradeitem, array('gradepass' => 50.0));
239         $gradeitem->update();
241         // With no grade, it should return true for INCOMPLETE and false for
242         // the other three.
243         $cond = new condition((object)array(
244                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
245         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
246         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
248         $cond = new condition((object)array(
249                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
250         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
251         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
253         // Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
254         $cond = new condition((object)array(
255                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
256         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
257         $information = $cond->get_description(false, false, $info);
258         $information = \core_availability\info::format_info($information, $course);
259         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
260         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
262         $cond = new condition((object)array(
263                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
264         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
265         $information = $cond->get_description(false, false, $info);
266         $information = \core_availability\info::format_info($information, $course);
267         $this->assertRegExp('~Assign!.*is complete and failed~', $information);
268         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
270         // Change the grade to be complete and failed.
271         self::set_grade($assignrow, $user->id, 40);
273         $cond = new condition((object)array(
274                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
275         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
276         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
278         $cond = new condition((object)array(
279                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
280         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
281         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
283         $cond = new condition((object)array(
284                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
285         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
286         $information = $cond->get_description(false, false, $info);
287         $information = \core_availability\info::format_info($information, $course);
288         $this->assertRegExp('~Assign!.*is complete and passed~', $information);
289         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
291         $cond = new condition((object)array(
292                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
293         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
294         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
295         $information = $cond->get_description(false, true, $info);
296         $information = \core_availability\info::format_info($information, $course);
297         $this->assertRegExp('~Assign!.*is not complete and failed~', $information);
299         // Now change it to pass.
300         self::set_grade($assignrow, $user->id, 60);
302         $cond = new condition((object)array(
303                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
304         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
305         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
307         $cond = new condition((object)array(
308                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
309         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
310         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
312         $cond = new condition((object)array(
313                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
314         $this->assertTrue($cond->is_available(false, $info, true, $user->id));
315         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
316         $information = $cond->get_description(false, true, $info);
317         $information = \core_availability\info::format_info($information, $course);
318         $this->assertRegExp('~Assign!.*is not complete and passed~', $information);
320         $cond = new condition((object)array(
321                 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
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 failed~', $information);
326         $this->assertTrue($cond->is_available(true, $info, true, $user->id));
328         // Simulate deletion of an activity by using an invalid cmid. These
329         // conditions always fail, regardless of NOT flag or INCOMPLETE.
330         $cond = new condition((object)array(
331                 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE));
332         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
333         $information = $cond->get_description(false, false, $info);
334         $information = \core_availability\info::format_info($information, $course);
335         $this->assertRegExp('~(Missing activity).*is marked complete~', $information);
336         $this->assertFalse($cond->is_available(true, $info, true, $user->id));
337         $cond = new condition((object)array(
338                 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE));
339         $this->assertFalse($cond->is_available(false, $info, true, $user->id));
340     }
342     /**
343      * Tests completion_value_used static function.
344      */
345     public function test_completion_value_used() {
346         global $CFG, $DB;
347         $this->resetAfterTest();
349         // Create course with completion turned on and some sections.
350         $CFG->enablecompletion = true;
351         $CFG->enableavailability = true;
352         $generator = $this->getDataGenerator();
353         $course = $generator->create_course(
354                 array('numsections' => 1, 'enablecompletion' => 1),
355                 array('createsections' => true));
356         availability_completion\condition::wipe_static_cache();
358         // Create three pages with manual completion.
359         $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
360                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
361         $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
362                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
363         $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
364                 array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
366         // Set up page3 to depend on page1, and section1 to depend on page2.
367         $DB->set_field('course_modules', 'availability',
368                 '{"op":"|","show":true,"c":[' .
369                 '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
370                 array('id' => $page3->cmid));
371         $DB->set_field('course_sections', 'availability',
372                 '{"op":"|","show":true,"c":[' .
373                 '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
374                 array('course' => $course->id, 'section' => 1));
376         // Now check: nothing depends on page3 but something does on the others.
377         $this->assertTrue(availability_completion\condition::completion_value_used(
378                 $course, $page1->cmid));
379         $this->assertTrue(availability_completion\condition::completion_value_used(
380                 $course, $page2->cmid));
381         $this->assertFalse(availability_completion\condition::completion_value_used(
382                 $course, $page3->cmid));
383     }
385     /**
386      * Updates the grade of a user in the given assign module instance.
387      *
388      * @param stdClass $assignrow Assignment row from database
389      * @param int $userid User id
390      * @param float $grade Grade
391      */
392     protected static function set_grade($assignrow, $userid, $grade) {
393         $grades = array();
394         $grades[$userid] = (object)array(
395                 'rawgrade' => $grade, 'userid' => $userid);
396         $assignrow->cmidnumber = null;
397         assign_grade_item_update($assignrow, $grades);
398     }
400     /**
401      * Tests the update_dependency_id() function.
402      */
403     public function test_update_dependency_id() {
404         $cond = new condition((object)array(
405                 'cm' => 123, 'e' => COMPLETION_COMPLETE));
406         $this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
407         $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
408         $this->assertTrue($cond->update_dependency_id('course_modules', 123, 456));
409         $after = $cond->save();
410         $this->assertEquals(456, $after->cm);
411     }