Merge branch 'MDL-63135-master' of git://github.com/rezaies/moodle
[moodle.git] / mod / choice / tests / lib_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  * Choice module library functions tests
19  *
20  * @package    mod_choice
21  * @category   test
22  * @copyright  2015 Juan Leyva <juan@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      Moodle 3.0
25  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
31 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32 require_once($CFG->dirroot . '/mod/choice/lib.php');
34 /**
35  * Choice module library functions tests
36  *
37  * @package    mod_choice
38  * @category   test
39  * @copyright  2015 Juan Leyva <juan@moodle.com>
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  * @since      Moodle 3.0
42  */
43 class mod_choice_lib_testcase extends externallib_advanced_testcase {
45     /**
46      * Test choice_view
47      * @return void
48      */
49     public function test_choice_view() {
50         global $CFG;
52         $this->resetAfterTest();
54         $this->setAdminUser();
55         // Setup test data.
56         $course = $this->getDataGenerator()->create_course();
57         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
58         $context = context_module::instance($choice->cmid);
59         $cm = get_coursemodule_from_instance('choice', $choice->id);
61         // Trigger and capture the event.
62         $sink = $this->redirectEvents();
64         choice_view($choice, $course, $cm, $context);
66         $events = $sink->get_events();
67         $this->assertCount(1, $events);
68         $event = array_shift($events);
70         // Checking that the event contains the expected values.
71         $this->assertInstanceOf('\mod_choice\event\course_module_viewed', $event);
72         $this->assertEquals($context, $event->get_context());
73         $url = new \moodle_url('/mod/choice/view.php', array('id' => $cm->id));
74         $this->assertEquals($url, $event->get_url());
75         $this->assertEventContextNotUsed($event);
76         $this->assertNotEmpty($event->get_name());
77     }
79     /**
80      * Test choice_can_view_results
81      * @return void
82      */
83     public function test_choice_can_view_results() {
84         global $DB, $USER;
86         $this->resetAfterTest();
88         $this->setAdminUser();
89         // Setup test data.
90         $course = $this->getDataGenerator()->create_course();
91         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
92         $context = context_module::instance($choice->cmid);
93         $cm = get_coursemodule_from_instance('choice', $choice->id);
95         // Default values are false, user cannot view results.
96         $canview = choice_can_view_results($choice);
97         $this->assertFalse($canview);
99         // Show results forced.
100         $choice->showresults = CHOICE_SHOWRESULTS_ALWAYS;
101         $DB->update_record('choice', $choice);
102         $canview = choice_can_view_results($choice);
103         $this->assertTrue($canview);
105         // Add a time restriction (choice not open yet).
106         $choice->timeopen = time() + YEARSECS;
107         $DB->update_record('choice', $choice);
108         $canview = choice_can_view_results($choice);
109         $this->assertFalse($canview);
111         // Show results after closing.
112         $choice->timeopen = 0;
113         $choice->showresults = CHOICE_SHOWRESULTS_AFTER_CLOSE;
114         $DB->update_record('choice', $choice);
115         $canview = choice_can_view_results($choice);
116         $this->assertFalse($canview);
118         $choice->timeclose = time() - HOURSECS;
119         $DB->update_record('choice', $choice);
120         $canview = choice_can_view_results($choice);
121         $this->assertTrue($canview);
123         // Show results after answering.
124         $choice->timeclose = 0;
125         $choice->showresults = CHOICE_SHOWRESULTS_AFTER_ANSWER;
126         $DB->update_record('choice', $choice);
127         $canview = choice_can_view_results($choice);
128         $this->assertFalse($canview);
130         // Get the first option.
131         $choicewithoptions = choice_get_choice($choice->id);
132         $optionids = array_keys($choicewithoptions->option);
134         choice_user_submit_response($optionids[0], $choice, $USER->id, $course, $cm);
136         $canview = choice_can_view_results($choice);
137         $this->assertTrue($canview);
139     }
141     /**
142      * @expectedException moodle_exception
143      */
144     public function test_choice_user_submit_response_validation() {
145         global $USER;
147         $this->resetAfterTest();
149         $this->setAdminUser();
150         // Setup test data.
151         $course = $this->getDataGenerator()->create_course();
152         $choice1 = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
153         $choice2 = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
154         $cm = get_coursemodule_from_instance('choice', $choice1->id);
156         $choicewithoptions1 = choice_get_choice($choice1->id);
157         $choicewithoptions2 = choice_get_choice($choice2->id);
158         $optionids1 = array_keys($choicewithoptions1->option);
159         $optionids2 = array_keys($choicewithoptions2->option);
161         // Make sure we cannot submit options from a different choice instance.
162         choice_user_submit_response($optionids2[0], $choice1, $USER->id, $course, $cm);
163     }
165     /**
166      * Test choice_get_user_response
167      * @return void
168      */
169     public function test_choice_get_user_response() {
170         $this->resetAfterTest();
172         $this->setAdminUser();
173         // Setup test data.
174         $course = $this->getDataGenerator()->create_course();
175         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
176         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
177         $cm = get_coursemodule_from_instance('choice', $choice->id);
179         $choicewithoptions = choice_get_choice($choice->id);
180         $optionids = array_keys($choicewithoptions->option);
182         choice_user_submit_response($optionids[0], $choice, $student->id, $course, $cm);
183         $responses = choice_get_user_response($choice, $student->id);
184         $this->assertCount(1, $responses);
185         $response = array_shift($responses);
186         $this->assertEquals($optionids[0], $response->optionid);
188         // Multiple responses.
189         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id, 'allowmultiple' => 1));
190         $cm = get_coursemodule_from_instance('choice', $choice->id);
192         $choicewithoptions = choice_get_choice($choice->id);
193         $optionids = array_keys($choicewithoptions->option);
195         // Submit a response with the options reversed.
196         $selections = $optionids;
197         rsort($selections);
198         choice_user_submit_response($selections, $choice, $student->id, $course, $cm);
199         $responses = choice_get_user_response($choice, $student->id);
200         $this->assertCount(count($optionids), $responses);
201         foreach ($responses as $resp) {
202             $this->assertEquals(array_shift($optionids), $resp->optionid);
203         }
204     }
206     /**
207      * Test choice_get_my_response
208      * @return void
209      */
210     public function test_choice_get_my_response() {
211         global $USER;
213         $this->resetAfterTest();
215         $this->setAdminUser();
216         // Setup test data.
217         $course = $this->getDataGenerator()->create_course();
218         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
219         $cm = get_coursemodule_from_instance('choice', $choice->id);
221         $choicewithoptions = choice_get_choice($choice->id);
222         $optionids = array_keys($choicewithoptions->option);
224         choice_user_submit_response($optionids[0], $choice, $USER->id, $course, $cm);
225         $responses = choice_get_my_response($choice);
226         $this->assertCount(1, $responses);
227         $response = array_shift($responses);
228         $this->assertEquals($optionids[0], $response->optionid);
230         // Multiple responses.
231         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id, 'allowmultiple' => 1));
232         $cm = get_coursemodule_from_instance('choice', $choice->id);
234         $choicewithoptions = choice_get_choice($choice->id);
235         $optionids = array_keys($choicewithoptions->option);
237         // Submit a response with the options reversed.
238         $selections = $optionids;
239         rsort($selections);
240         choice_user_submit_response($selections, $choice, $USER->id, $course, $cm);
241         $responses = choice_get_my_response($choice);
242         $this->assertCount(count($optionids), $responses);
243         foreach ($responses as $resp) {
244             $this->assertEquals(array_shift($optionids), $resp->optionid);
245         }
246     }
248     /**
249      * Test choice_get_availability_status
250      * @return void
251      */
252     public function test_choice_get_availability_status() {
253         global $USER;
255         $this->resetAfterTest();
257         $this->setAdminUser();
258         // Setup test data.
259         $course = $this->getDataGenerator()->create_course();
260         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
262         // No time restrictions and updates allowed.
263         list($status, $warnings) = choice_get_availability_status($choice, false);
264         $this->assertEquals(true, $status);
265         $this->assertCount(0, $warnings);
267         // No updates allowed, but haven't answered yet.
268         $choice->allowupdate = false;
269         list($status, $warnings) = choice_get_availability_status($choice, false);
270         $this->assertEquals(true, $status);
271         $this->assertCount(0, $warnings);
273         // No updates allowed and have answered.
274         $cm = get_coursemodule_from_instance('choice', $choice->id);
275         $choicewithoptions = choice_get_choice($choice->id);
276         $optionids = array_keys($choicewithoptions->option);
277         choice_user_submit_response($optionids[0], $choice, $USER->id, $course, $cm);
278         list($status, $warnings) = choice_get_availability_status($choice, false);
279         $this->assertEquals(false, $status);
280         $this->assertCount(1, $warnings);
281         $this->assertEquals('choicesaved', array_keys($warnings)[0]);
283         $choice->allowupdate = true;
285         // With time restrictions, still open.
286         $choice->timeopen = time() - DAYSECS;
287         $choice->timeclose = time() + DAYSECS;
288         list($status, $warnings) = choice_get_availability_status($choice, false);
289         $this->assertEquals(true, $status);
290         $this->assertCount(0, $warnings);
292         // Choice not open yet.
293         $choice->timeopen = time() + DAYSECS;
294         $choice->timeclose = $choice->timeopen + DAYSECS;
295         list($status, $warnings) = choice_get_availability_status($choice, false);
296         $this->assertEquals(false, $status);
297         $this->assertCount(1, $warnings);
298         $this->assertEquals('notopenyet', array_keys($warnings)[0]);
300         // Choice closed.
301         $choice->timeopen = time() - DAYSECS;
302         $choice->timeclose = time() - 1;
303         list($status, $warnings) = choice_get_availability_status($choice, false);
304         $this->assertEquals(false, $status);
305         $this->assertCount(1, $warnings);
306         $this->assertEquals('expired', array_keys($warnings)[0]);
307     }
309     /*
310      * The choice's event should not be shown to a user when the user cannot view the choice activity at all.
311      */
312     public function test_choice_core_calendar_provide_event_action_in_hidden_section() {
313         global $CFG;
315         $this->resetAfterTest();
317         $this->setAdminUser();
319         // Create a course.
320         $course = $this->getDataGenerator()->create_course();
322         // Create a student.
323         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
325         // Create a choice.
326         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
327                 'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
329         // Create a calendar event.
330         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
332         // Set sections 0 as hidden.
333         set_section_visible($course->id, 0, 0);
335         // Now, log out.
336         $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
337         $this->setUser();
339         // Create an action factory.
340         $factory = new \core_calendar\action_factory();
342         // Decorate action event for the student.
343         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
345         // Confirm the event is not shown at all.
346         $this->assertNull($actionevent);
347     }
349     /*
350      * The choice's event should not be shown to a user who does not have permission to view the choice.
351      */
352     public function test_choice_core_calendar_provide_event_action_for_non_user() {
353         global $CFG;
355         $this->resetAfterTest();
357         $this->setAdminUser();
359         // Create a course.
360         $course = $this->getDataGenerator()->create_course();
362         // Create a choice.
363         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
364                 'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
366         // Create a calendar event.
367         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
369         // Now, log out.
370         $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
371         $this->setUser();
373         // Create an action factory.
374         $factory = new \core_calendar\action_factory();
376         // Decorate action event.
377         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory);
379         // Confirm the event is not shown at all.
380         $this->assertNull($actionevent);
381     }
383     public function test_choice_core_calendar_provide_event_action_open() {
384         $this->resetAfterTest();
386         $this->setAdminUser();
388         // Create a course.
389         $course = $this->getDataGenerator()->create_course();
391         // Create a choice.
392         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
393             'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
395         // Create a calendar event.
396         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
398         // Create an action factory.
399         $factory = new \core_calendar\action_factory();
401         // Decorate action event.
402         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory);
404         // Confirm the event was decorated.
405         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
406         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
407         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
408         $this->assertEquals(1, $actionevent->get_item_count());
409         $this->assertTrue($actionevent->is_actionable());
410     }
412     public function test_choice_core_calendar_provide_event_action_open_for_user() {
413         $this->resetAfterTest();
415         $this->setAdminUser();
417         // Create a course.
418         $course = $this->getDataGenerator()->create_course();
420         // Create a student.
421         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
423         // Create a choice.
424         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
425             'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
427         // Create a calendar event.
428         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
430         // Create an action factory.
431         $factory = new \core_calendar\action_factory();
433         // Decorate action event for the student.
434         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
436         // Confirm the event was decorated.
437         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
438         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
439         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
440         $this->assertEquals(1, $actionevent->get_item_count());
441         $this->assertTrue($actionevent->is_actionable());
442     }
444     /**
445      * An event should not have an action if the user has already submitted a response
446      * to the choice activity.
447      */
448     public function test_choice_core_calendar_provide_event_action_already_submitted() {
449         $this->resetAfterTest();
451         $this->setAdminUser();
453         // Create a course.
454         $course = $this->getDataGenerator()->create_course();
456         // Create a student.
457         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
459         // Create a choice.
460         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
461             'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
462         $cm = get_coursemodule_from_instance('choice', $choice->id);
464         $choicewithoptions = choice_get_choice($choice->id);
465         $optionids = array_keys($choicewithoptions->option);
467         choice_user_submit_response($optionids[0], $choice, $student->id, $course, $cm);
469         // Create a calendar event.
470         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
472         // Create an action factory.
473         $factory = new \core_calendar\action_factory();
475         $this->setUser($student);
477         // Decorate action event.
478         $action = mod_choice_core_calendar_provide_event_action($event, $factory);
480         // Confirm no action was returned if the user has already submitted the
481         // choice activity.
482         $this->assertNull($action);
483     }
485     /**
486      * An event should not have an action if the user has already submitted a response
487      * to the choice activity.
488      */
489     public function test_choice_core_calendar_provide_event_action_already_submitted_for_user() {
490         $this->resetAfterTest();
492         $this->setAdminUser();
494         // Create a course.
495         $course = $this->getDataGenerator()->create_course();
497         // Create a student.
498         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
500         // Create a choice.
501         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
502             'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
503         $cm = get_coursemodule_from_instance('choice', $choice->id);
505         $choicewithoptions = choice_get_choice($choice->id);
506         $optionids = array_keys($choicewithoptions->option);
508         choice_user_submit_response($optionids[0], $choice, $student->id, $course, $cm);
510         // Create a calendar event.
511         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
513         // Create an action factory.
514         $factory = new \core_calendar\action_factory();
516         // Decorate action event for the student.
517         $action = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
519         // Confirm no action was returned if the user has already submitted the
520         // choice activity.
521         $this->assertNull($action);
522     }
524     public function test_choice_core_calendar_provide_event_action_closed() {
525         $this->resetAfterTest();
527         $this->setAdminUser();
529         // Create a course.
530         $course = $this->getDataGenerator()->create_course();
532         $timeclose = time() - DAYSECS;
533         // Create a choice.
534         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
535             'timeclose' => $timeclose));
537         // Create a calendar event.
538         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeclose - 1);
540         // Create an action factory.
541         $factory = new \core_calendar\action_factory();
543         // Decorate action event.
544         $action = mod_choice_core_calendar_provide_event_action($event, $factory);
546         // Confirm not action was provided for a closed activity.
547         $this->assertNull($action);
548     }
550     public function test_choice_core_calendar_provide_event_action_closed_for_user() {
551         $this->resetAfterTest();
553         $this->setAdminUser();
555         // Create a course.
556         $course = $this->getDataGenerator()->create_course();
558         // Create a student.
559         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
561         $timeclose = time() - DAYSECS;
562         // Create a choice.
563         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
564             'timeclose' => $timeclose));
566         // Create a calendar event.
567         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeclose - 1);
569         // Create an action factory.
570         $factory = new \core_calendar\action_factory();
572         // Decorate action event for the student.
573         $action = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
575         // Confirm not action was provided for a closed activity.
576         $this->assertNull($action);
577     }
579     public function test_choice_core_calendar_provide_event_action_open_in_future() {
580         $this->resetAfterTest();
582         $this->setAdminUser();
584         // Create a course.
585         $course = $this->getDataGenerator()->create_course();
587         $timeopen = time() + DAYSECS;
588         $timeclose = $timeopen + DAYSECS;
590         // Create a choice.
591         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
592             'timeopen' => $timeopen, 'timeclose' => $timeclose));
594         // Create a calendar event.
595         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeopen);
597         // Create an action factory.
598         $factory = new \core_calendar\action_factory();
600         // Decorate action event.
601         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory);
603         // Confirm the event was decorated.
604         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
605         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
606         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
607         $this->assertEquals(1, $actionevent->get_item_count());
608         $this->assertFalse($actionevent->is_actionable());
609     }
611     public function test_choice_core_calendar_provide_event_action_open_in_future_for_user() {
612         global $CFG;
614         $this->resetAfterTest();
616         $this->setAdminUser();
618         // Create a course.
619         $course = $this->getDataGenerator()->create_course();
621         // Create a student.
622         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
624         $timeopen = time() + DAYSECS;
625         $timeclose = $timeopen + DAYSECS;
627         // Create a choice.
628         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
629             'timeopen' => $timeopen, 'timeclose' => $timeclose));
631         // Create a calendar event.
632         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeopen);
634         // Now, log out.
635         $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
636         $this->setUser();
638         // Create an action factory.
639         $factory = new \core_calendar\action_factory();
641         // Decorate action event for the student.
642         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
644         // Confirm the event was decorated.
645         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
646         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
647         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
648         $this->assertEquals(1, $actionevent->get_item_count());
649         $this->assertFalse($actionevent->is_actionable());
650     }
652     public function test_choice_core_calendar_provide_event_action_no_time_specified() {
653         $this->resetAfterTest();
655         $this->setAdminUser();
657         // Create a course.
658         $course = $this->getDataGenerator()->create_course();
660         // Create a choice.
661         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
663         // Create a calendar event.
664         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
666         // Create an action factory.
667         $factory = new \core_calendar\action_factory();
669         // Decorate action event.
670         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory);
672         // Confirm the event was decorated.
673         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
674         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
675         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
676         $this->assertEquals(1, $actionevent->get_item_count());
677         $this->assertTrue($actionevent->is_actionable());
678     }
680     public function test_choice_core_calendar_provide_event_action_no_time_specified_for_user() {
681         global $CFG;
683         $this->resetAfterTest();
685         $this->setAdminUser();
687         // Create a course.
688         $course = $this->getDataGenerator()->create_course();
690         // Create a student.
691         $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
693         // Create a choice.
694         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
696         // Create a calendar event.
697         $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
699         // Now, log out.
700         $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities.
701         $this->setUser();
703         // Create an action factory.
704         $factory = new \core_calendar\action_factory();
706         // Decorate action event for the student.
707         $actionevent = mod_choice_core_calendar_provide_event_action($event, $factory, $student->id);
709         // Confirm the event was decorated.
710         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
711         $this->assertEquals(get_string('viewchoices', 'choice'), $actionevent->get_name());
712         $this->assertInstanceOf('moodle_url', $actionevent->get_url());
713         $this->assertEquals(1, $actionevent->get_item_count());
714         $this->assertTrue($actionevent->is_actionable());
715     }
717     /**
718      * Creates an action event.
719      *
720      * @param int $courseid
721      * @param int $instanceid The choice id.
722      * @param string $eventtype The event type. eg. CHOICE_EVENT_TYPE_OPEN.
723      * @param int|null $timestart The start timestamp for the event
724      * @return bool|calendar_event
725      */
726     private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
727         $event = new stdClass();
728         $event->name = 'Calendar event';
729         $event->modulename = 'choice';
730         $event->courseid = $courseid;
731         $event->instance = $instanceid;
732         $event->type = CALENDAR_EVENT_TYPE_ACTION;
733         $event->eventtype = $eventtype;
735         if ($timestart) {
736             $event->timestart = $timestart;
737         } else {
738             $event->timestart = time();
739         }
741         return calendar_event::create($event);
742     }
744     /**
745      * Test the callback responsible for returning the completion rule descriptions.
746      * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
747      * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
748      */
749     public function test_mod_choice_completion_get_active_rule_descriptions() {
750         $this->resetAfterTest();
751         $this->setAdminUser();
753         // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
754         $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
755         $choice1 = $this->getDataGenerator()->create_module('choice', [
756             'course' => $course->id,
757             'completion' => 2,
758             'completionsubmit' => 1
759         ]);
760         $choice2 = $this->getDataGenerator()->create_module('choice', [
761             'course' => $course->id,
762             'completion' => 2,
763             'completionsubmit' => 0
764         ]);
765         $cm1 = cm_info::create(get_coursemodule_from_instance('choice', $choice1->id));
766         $cm2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
768         // Data for the stdClass input type.
769         // This type of input would occur when checking the default completion rules for an activity type, where we don't have
770         // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
771         $moddefaults = new stdClass();
772         $moddefaults->customdata = ['customcompletionrules' => ['completionsubmit' => 1]];
773         $moddefaults->completion = 2;
775         $activeruledescriptions = [get_string('completionsubmit', 'choice')];
776         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
777         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions($cm2), []);
778         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
779         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions(new stdClass()), []);
780     }
782     /**
783      * An unkown event type should not change the choice instance.
784      */
785     public function test_mod_choice_core_calendar_event_timestart_updated_unknown_event() {
786         global $CFG, $DB;
787         require_once($CFG->dirroot . "/calendar/lib.php");
789         $this->resetAfterTest(true);
790         $this->setAdminUser();
791         $generator = $this->getDataGenerator();
792         $course = $generator->create_course();
793         $choicegenerator = $generator->get_plugin_generator('mod_choice');
794         $timeopen = time();
795         $timeclose = $timeopen + DAYSECS;
796         $choice = $choicegenerator->create_instance(['course' => $course->id]);
797         $choice->timeopen = $timeopen;
798         $choice->timeclose = $timeclose;
799         $DB->update_record('choice', $choice);
801         // Create a valid event.
802         $event = new \calendar_event([
803             'name' => 'Test event',
804             'description' => '',
805             'format' => 1,
806             'courseid' => $course->id,
807             'groupid' => 0,
808             'userid' => 2,
809             'modulename' => 'choice',
810             'instance' => $choice->id,
811             'eventtype' => CHOICE_EVENT_TYPE_OPEN . "SOMETHING ELSE",
812             'timestart' => 1,
813             'timeduration' => 86400,
814             'visible' => 1
815         ]);
817         mod_choice_core_calendar_event_timestart_updated($event, $choice);
819         $choice = $DB->get_record('choice', ['id' => $choice->id]);
820         $this->assertEquals($timeopen, $choice->timeopen);
821         $this->assertEquals($timeclose, $choice->timeclose);
822     }
824     /**
825      * A CHOICE_EVENT_TYPE_OPEN event should update the timeopen property of
826      * the choice activity.
827      */
828     public function test_mod_choice_core_calendar_event_timestart_updated_open_event() {
829         global $CFG, $DB;
830         require_once($CFG->dirroot . "/calendar/lib.php");
832         $this->resetAfterTest(true);
833         $this->setAdminUser();
834         $generator = $this->getDataGenerator();
835         $course = $generator->create_course();
836         $choicegenerator = $generator->get_plugin_generator('mod_choice');
837         $timeopen = time();
838         $timeclose = $timeopen + DAYSECS;
839         $timemodified = 1;
840         $newtimeopen = $timeopen - DAYSECS;
841         $choice = $choicegenerator->create_instance(['course' => $course->id]);
842         $choice->timeopen = $timeopen;
843         $choice->timeclose = $timeclose;
844         $choice->timemodified = $timemodified;
845         $DB->update_record('choice', $choice);
847         // Create a valid event.
848         $event = new \calendar_event([
849             'name' => 'Test event',
850             'description' => '',
851             'format' => 1,
852             'courseid' => $course->id,
853             'groupid' => 0,
854             'userid' => 2,
855             'modulename' => 'choice',
856             'instance' => $choice->id,
857             'eventtype' => CHOICE_EVENT_TYPE_OPEN,
858             'timestart' => $newtimeopen,
859             'timeduration' => 86400,
860             'visible' => 1
861         ]);
863         // Trigger and capture the event when adding a contact.
864         $sink = $this->redirectEvents();
866         mod_choice_core_calendar_event_timestart_updated($event, $choice);
868         $triggeredevents = $sink->get_events();
869         $moduleupdatedevents = array_filter($triggeredevents, function($e) {
870             return is_a($e, 'core\event\course_module_updated');
871         });
873         $choice = $DB->get_record('choice', ['id' => $choice->id]);
874         // Ensure the timeopen property matches the event timestart.
875         $this->assertEquals($newtimeopen, $choice->timeopen);
876         // Ensure the timeclose isn't changed.
877         $this->assertEquals($timeclose, $choice->timeclose);
878         // Ensure the timemodified property has been changed.
879         $this->assertNotEquals($timemodified, $choice->timemodified);
880         // Confirm that a module updated event is fired when the module
881         // is changed.
882         $this->assertNotEmpty($moduleupdatedevents);
883     }
885     /**
886      * A CHOICE_EVENT_TYPE_CLOSE event should update the timeclose property of
887      * the choice activity.
888      */
889     public function test_mod_choice_core_calendar_event_timestart_updated_close_event() {
890         global $CFG, $DB;
891         require_once($CFG->dirroot . "/calendar/lib.php");
893         $this->resetAfterTest(true);
894         $this->setAdminUser();
895         $generator = $this->getDataGenerator();
896         $course = $generator->create_course();
897         $choicegenerator = $generator->get_plugin_generator('mod_choice');
898         $timeopen = time();
899         $timeclose = $timeopen + DAYSECS;
900         $timemodified = 1;
901         $newtimeclose = $timeclose + DAYSECS;
902         $choice = $choicegenerator->create_instance(['course' => $course->id]);
903         $choice->timeopen = $timeopen;
904         $choice->timeclose = $timeclose;
905         $choice->timemodified = $timemodified;
906         $DB->update_record('choice', $choice);
908         // Create a valid event.
909         $event = new \calendar_event([
910             'name' => 'Test event',
911             'description' => '',
912             'format' => 1,
913             'courseid' => $course->id,
914             'groupid' => 0,
915             'userid' => 2,
916             'modulename' => 'choice',
917             'instance' => $choice->id,
918             'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
919             'timestart' => $newtimeclose,
920             'timeduration' => 86400,
921             'visible' => 1
922         ]);
924         // Trigger and capture the event when adding a contact.
925         $sink = $this->redirectEvents();
927         mod_choice_core_calendar_event_timestart_updated($event, $choice);
929         $triggeredevents = $sink->get_events();
930         $moduleupdatedevents = array_filter($triggeredevents, function($e) {
931             return is_a($e, 'core\event\course_module_updated');
932         });
934         $choice = $DB->get_record('choice', ['id' => $choice->id]);
935         // Ensure the timeclose property matches the event timestart.
936         $this->assertEquals($newtimeclose, $choice->timeclose);
937         // Ensure the timeopen isn't changed.
938         $this->assertEquals($timeopen, $choice->timeopen);
939         // Ensure the timemodified property has been changed.
940         $this->assertNotEquals($timemodified, $choice->timemodified);
941         // Confirm that a module updated event is fired when the module
942         // is changed.
943         $this->assertNotEmpty($moduleupdatedevents);
944     }
946     /**
947      * An unkown event type should not have any limits
948      */
949     public function test_mod_choice_core_calendar_get_valid_event_timestart_range_unknown_event() {
950         global $CFG, $DB;
951         require_once($CFG->dirroot . "/calendar/lib.php");
953         $this->resetAfterTest(true);
954         $this->setAdminUser();
955         $generator = $this->getDataGenerator();
956         $course = $generator->create_course();
957         $timeopen = time();
958         $timeclose = $timeopen + DAYSECS;
959         $choice = new \stdClass();
960         $choice->timeopen = $timeopen;
961         $choice->timeclose = $timeclose;
963         // Create a valid event.
964         $event = new \calendar_event([
965             'name' => 'Test event',
966             'description' => '',
967             'format' => 1,
968             'courseid' => $course->id,
969             'groupid' => 0,
970             'userid' => 2,
971             'modulename' => 'choice',
972             'instance' => 1,
973             'eventtype' => CHOICE_EVENT_TYPE_OPEN . "SOMETHING ELSE",
974             'timestart' => 1,
975             'timeduration' => 86400,
976             'visible' => 1
977         ]);
979         list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
980         $this->assertNull($min);
981         $this->assertNull($max);
982     }
984     /**
985      * The open event should be limited by the choice's timeclose property, if it's set.
986      */
987     public function test_mod_choice_core_calendar_get_valid_event_timestart_range_open_event() {
988         global $CFG, $DB;
989         require_once($CFG->dirroot . "/calendar/lib.php");
991         $this->resetAfterTest(true);
992         $this->setAdminUser();
993         $generator = $this->getDataGenerator();
994         $course = $generator->create_course();
995         $timeopen = time();
996         $timeclose = $timeopen + DAYSECS;
997         $choice = new \stdClass();
998         $choice->timeopen = $timeopen;
999         $choice->timeclose = $timeclose;
1001         // Create a valid event.
1002         $event = new \calendar_event([
1003             'name' => 'Test event',
1004             'description' => '',
1005             'format' => 1,
1006             'courseid' => $course->id,
1007             'groupid' => 0,
1008             'userid' => 2,
1009             'modulename' => 'choice',
1010             'instance' => 1,
1011             'eventtype' => CHOICE_EVENT_TYPE_OPEN,
1012             'timestart' => 1,
1013             'timeduration' => 86400,
1014             'visible' => 1
1015         ]);
1017         // The max limit should be bounded by the timeclose value.
1018         list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
1020         $this->assertNull($min);
1021         $this->assertEquals($timeclose, $max[0]);
1023         // No timeclose value should result in no upper limit.
1024         $choice->timeclose = 0;
1025         list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
1027         $this->assertNull($min);
1028         $this->assertNull($max);
1029     }
1031     /**
1032      * The close event should be limited by the choice's timeopen property, if it's set.
1033      */
1034     public function test_mod_choice_core_calendar_get_valid_event_timestart_range_close_event() {
1035         global $CFG, $DB;
1036         require_once($CFG->dirroot . "/calendar/lib.php");
1038         $this->resetAfterTest(true);
1039         $this->setAdminUser();
1040         $generator = $this->getDataGenerator();
1041         $course = $generator->create_course();
1042         $timeopen = time();
1043         $timeclose = $timeopen + DAYSECS;
1044         $choice = new \stdClass();
1045         $choice->timeopen = $timeopen;
1046         $choice->timeclose = $timeclose;
1048         // Create a valid event.
1049         $event = new \calendar_event([
1050             'name' => 'Test event',
1051             'description' => '',
1052             'format' => 1,
1053             'courseid' => $course->id,
1054             'groupid' => 0,
1055             'userid' => 2,
1056             'modulename' => 'choice',
1057             'instance' => 1,
1058             'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
1059             'timestart' => 1,
1060             'timeduration' => 86400,
1061             'visible' => 1
1062         ]);
1064         // The max limit should be bounded by the timeclose value.
1065         list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
1067         $this->assertEquals($timeopen, $min[0]);
1068         $this->assertNull($max);
1070         // No timeclose value should result in no upper limit.
1071         $choice->timeopen = 0;
1072         list ($min, $max) = mod_choice_core_calendar_get_valid_event_timestart_range($event, $choice);
1074         $this->assertNull($min);
1075         $this->assertNull($max);
1076     }
1078     /**
1079      * Test choice_user_submit_response for a choice with specific options.
1080      * Options:
1081      * allowmultiple: false
1082      * limitanswers: false
1083      */
1084     public function test_choice_user_submit_response_no_multiple_no_limits() {
1085         global $DB;
1086         $this->resetAfterTest(true);
1088         $generator = $this->getDataGenerator();
1089         $course = $generator->create_course();
1090         $user = $generator->create_user();
1091         $user2 = $generator->create_user();
1093         // User must be enrolled in the course for choice limits to be honoured properly.
1094         $role = $DB->get_record('role', ['shortname' => 'student']);
1095         $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
1096         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
1098         // Create choice, with updates allowed and a two options both limited to 1 response each.
1099         $choice = $generator->get_plugin_generator('mod_choice')->create_instance([
1100             'course' => $course->id,
1101             'allowupdate' => false,
1102             'limitanswers' => false,
1103             'allowmultiple' => false,
1104             'option' => ['red', 'green'],
1105         ]);
1106         $cm = get_coursemodule_from_instance('choice', $choice->id);
1108         // Get the choice, with options and limits included.
1109         $choicewithoptions = choice_get_choice($choice->id);
1110         $optionids = array_keys($choicewithoptions->option);
1112         // Now, save an response which includes the first option.
1113         $this->assertNull(choice_user_submit_response($optionids[0], $choicewithoptions, $user->id, $course, $cm));
1115         // Confirm that saving again without changing the selected option will not throw a 'choice full' exception.
1116         $this->assertNull(choice_user_submit_response($optionids[1], $choicewithoptions, $user->id, $course, $cm));
1118         // Confirm that saving a response for student 2 including the first option is allowed.
1119         $this->assertNull(choice_user_submit_response($optionids[0], $choicewithoptions, $user2->id, $course, $cm));
1121         // Confirm that trying to save multiple options results in an exception.
1122         $this->expectException('moodle_exception');
1123         choice_user_submit_response([$optionids[1], $optionids[1]], $choicewithoptions, $user->id, $course, $cm);
1124     }
1126     /**
1127      * Test choice_user_submit_response for a choice with specific options.
1128      * Options:
1129      * allowmultiple: true
1130      * limitanswers: false
1131      */
1132     public function test_choice_user_submit_response_multiples_no_limits() {
1133         global $DB;
1134         $this->resetAfterTest(true);
1136         $generator = $this->getDataGenerator();
1137         $course = $generator->create_course();
1138         $user = $generator->create_user();
1139         $user2 = $generator->create_user();
1141         // User must be enrolled in the course for choice limits to be honoured properly.
1142         $role = $DB->get_record('role', ['shortname' => 'student']);
1143         $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
1144         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
1146         // Create choice, with updates allowed and a two options both limited to 1 response each.
1147         $choice = $generator->get_plugin_generator('mod_choice')->create_instance([
1148             'course' => $course->id,
1149             'allowupdate' => false,
1150             'allowmultiple' => true,
1151             'limitanswers' => false,
1152             'option' => ['red', 'green'],
1153         ]);
1154         $cm = get_coursemodule_from_instance('choice', $choice->id);
1156         // Get the choice, with options and limits included.
1157         $choicewithoptions = choice_get_choice($choice->id);
1158         $optionids = array_keys($choicewithoptions->option);
1160         // Save a response which includes the first option only.
1161         $this->assertNull(choice_user_submit_response([$optionids[0]], $choicewithoptions, $user->id, $course, $cm));
1163         // Confirm that adding an option to the response is allowed.
1164         $this->assertNull(choice_user_submit_response([$optionids[0], $optionids[1]], $choicewithoptions, $user->id, $course, $cm));
1166         // Confirm that saving a response for student 2 including the first option is allowed.
1167         $this->assertNull(choice_user_submit_response($optionids[0], $choicewithoptions, $user2->id, $course, $cm));
1169         // Confirm that removing an option from the response is allowed.
1170         $this->assertNull(choice_user_submit_response([$optionids[0]], $choicewithoptions, $user->id, $course, $cm));
1172         // Confirm that removing all options from the response is not allowed via this method.
1173         $this->expectException('moodle_exception');
1174         choice_user_submit_response([], $choicewithoptions, $user->id, $course, $cm);
1175     }
1177     /**
1178      * Test choice_user_submit_response for a choice with specific options.
1179      * Options:
1180      * allowmultiple: false
1181      * limitanswers: true
1182      */
1183     public function test_choice_user_submit_response_no_multiples_limits() {
1184         global $DB;
1185         $this->resetAfterTest(true);
1187         $generator = $this->getDataGenerator();
1188         $course = $generator->create_course();
1189         $user = $generator->create_user();
1190         $user2 = $generator->create_user();
1192         // User must be enrolled in the course for choice limits to be honoured properly.
1193         $role = $DB->get_record('role', ['shortname' => 'student']);
1194         $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
1195         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
1197         // Create choice, with updates allowed and a two options both limited to 1 response each.
1198         $choice = $generator->get_plugin_generator('mod_choice')->create_instance([
1199             'course' => $course->id,
1200             'allowupdate' => false,
1201             'allowmultiple' => false,
1202             'limitanswers' => true,
1203             'option' => ['red', 'green'],
1204             'limit' => [1, 1]
1205         ]);
1206         $cm = get_coursemodule_from_instance('choice', $choice->id);
1208         // Get the choice, with options and limits included.
1209         $choicewithoptions = choice_get_choice($choice->id);
1210         $optionids = array_keys($choicewithoptions->option);
1212         // Save a response which includes the first option only.
1213         $this->assertNull(choice_user_submit_response($optionids[0], $choicewithoptions, $user->id, $course, $cm));
1215         // Confirm that changing the option in the response is allowed.
1216         $this->assertNull(choice_user_submit_response($optionids[1], $choicewithoptions, $user->id, $course, $cm));
1218         // Confirm that limits are respected by trying to save the same option as another user.
1219         $this->expectException('moodle_exception');
1220         choice_user_submit_response($optionids[1], $choicewithoptions, $user2->id, $course, $cm);
1221     }
1223     /**
1224      * Test choice_user_submit_response for a choice with specific options.
1225      * Options:
1226      * allowmultiple: true
1227      * limitanswers: true
1228      */
1229     public function test_choice_user_submit_response_multiples_limits() {
1230         global $DB;
1231         $this->resetAfterTest(true);
1233         $generator = $this->getDataGenerator();
1234         $course = $generator->create_course();
1235         $user = $generator->create_user();
1236         $user2 = $generator->create_user();
1238         // User must be enrolled in the course for choice limits to be honoured properly.
1239         $role = $DB->get_record('role', ['shortname' => 'student']);
1240         $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
1241         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
1243         // Create choice, with updates allowed and a two options both limited to 1 response each.
1244         $choice = $generator->get_plugin_generator('mod_choice')->create_instance([
1245             'course' => $course->id,
1246             'allowupdate' => false,
1247             'allowmultiple' => true,
1248             'limitanswers' => true,
1249             'option' => ['red', 'green'],
1250             'limit' => [1, 1]
1251         ]);
1252         $cm = get_coursemodule_from_instance('choice', $choice->id);
1254         // Get the choice, with options and limits included.
1255         $choicewithoptions = choice_get_choice($choice->id);
1256         $optionids = array_keys($choicewithoptions->option);
1258         // Now, save a response which includes the first option only.
1259         $this->assertNull(choice_user_submit_response([$optionids[0]], $choicewithoptions, $user->id, $course, $cm));
1261         // Confirm that changing the option in the response is allowed.
1262         $this->assertNull(choice_user_submit_response([$optionids[1]], $choicewithoptions, $user->id, $course, $cm));
1264         // Confirm that adding an option to the response is allowed.
1265         $this->assertNull(choice_user_submit_response([$optionids[0], $optionids[1]], $choicewithoptions, $user->id, $course, $cm));
1267         // Confirm that limits are respected by trying to save the same option as another user.
1268         $this->expectException('moodle_exception');
1269         choice_user_submit_response($optionids[1], $choicewithoptions, $user2->id, $course, $cm);
1270     }
1272     /**
1273      * A user who does not have capabilities to add events to the calendar should be able to create an choice.
1274      */
1275     public function test_creation_with_no_calendar_capabilities() {
1276         $this->resetAfterTest();
1277         $course = self::getDataGenerator()->create_course();
1278         $context = context_course::instance($course->id);
1279         $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
1280         $roleid = self::getDataGenerator()->create_role();
1281         self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
1282         assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
1283         $generator = self::getDataGenerator()->get_plugin_generator('mod_choice');
1284         // Create an instance as a user without the calendar capabilities.
1285         $this->setUser($user);
1286         $time = time();
1287         $params = array(
1288             'course' => $course->id,
1289             'timeopen' => $time + 200,
1290             'timeclose' => $time + 500,
1291         );
1292         $generator->create_instance($params);
1293     }