1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16/**
17 * Unit tests for (some of) mod/feedback/lib.php.
18 *
19 * @package    mod_feedback
20 * @copyright  2016 Stephen Bourget
21 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23defined('MOODLE_INTERNAL') || die();
24global $CFG;
25require_once($CFG->dirroot . '/mod/feedback/lib.php');
26
27/**
28 * Unit tests for (some of) mod/feedback/lib.php.
29 *
30 * @copyright  2016 Stephen Bourget
31 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32 */
33class mod_feedback_lib_testcase extends advanced_testcase {
34
35    public function test_feedback_initialise() {
36        $this->resetAfterTest();
37        $this->setAdminUser();
38
39        $course = $this->getDataGenerator()->create_course();
40        $params['course'] = $course->id;
41        $params['timeopen'] = time() - 5 * MINSECS;
42        $params['timeclose'] = time() + DAYSECS;
43        $params['anonymous'] = 1;
44        $params['intro'] = 'Some introduction text';
45        $feedback = $this->getDataGenerator()->create_module('feedback', $params);
46
47        // Test different ways to construct the structure object.
48        $pseudocm = get_coursemodule_from_instance('feedback', $feedback->id); // Object similar to cm_info.
49        $cm = get_fast_modinfo($course)->instances['feedback'][$feedback->id]; // Instance of cm_info.
50
51        $constructorparams = [
52            [$feedback, null],
53            [null, $pseudocm],
54            [null, $cm],
55            [$feedback, $pseudocm],
56            [$feedback, $cm],
57        ];
58
59        foreach ($constructorparams as $params) {
60            $structure = new mod_feedback_completion($params[0], $params[1], 0);
61            $this->assertTrue($structure->is_open());
62            $this->assertTrue($structure->get_cm() instanceof cm_info);
63            $this->assertEquals($feedback->cmid, $structure->get_cm()->id);
64            $this->assertEquals($feedback->intro, $structure->get_feedback()->intro);
65        }
66    }
67
68    /**
69     * Tests for mod_feedback_refresh_events.
70     */
71    public function test_feedback_refresh_events() {
72        global $DB;
73        $this->resetAfterTest();
74        $this->setAdminUser();
75
76        $timeopen = time();
77        $timeclose = time() + 86400;
78
79        $course = $this->getDataGenerator()->create_course();
80        $generator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
81        $params['course'] = $course->id;
82        $params['timeopen'] = $timeopen;
83        $params['timeclose'] = $timeclose;
84        $feedback = $generator->create_instance($params);
85        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
86        $context = context_module::instance($cm->id);
87
88        // Normal case, with existing course.
89        $this->assertTrue(feedback_refresh_events($course->id));
90        $eventparams = array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => 'open');
91        $openevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST);
92        $this->assertEquals($openevent->timestart, $timeopen);
93
94        $eventparams = array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => 'close');
95        $closeevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST);
96        $this->assertEquals($closeevent->timestart, $timeclose);
97        // In case the course ID is passed as a numeric string.
98        $this->assertTrue(feedback_refresh_events('' . $course->id));
99        // Course ID not provided.
100        $this->assertTrue(feedback_refresh_events());
101        $eventparams = array('modulename' => 'feedback');
102        $events = $DB->get_records('event', $eventparams);
103        foreach ($events as $event) {
104            if ($event->modulename === 'feedback' && $event->instance === $feedback->id && $event->eventtype === 'open') {
105                $this->assertEquals($event->timestart, $timeopen);
106            }
107            if ($event->modulename === 'feedback' && $event->instance === $feedback->id && $event->eventtype === 'close') {
108                $this->assertEquals($event->timestart, $timeclose);
109            }
110        }
111    }
112
113    /**
114     * Test check_updates_since callback.
115     */
116    public function test_check_updates_since() {
117        global $DB;
118
119        $this->resetAfterTest();
120        $this->setAdminUser();
121        $course = $this->getDataGenerator()->create_course();
122
123        // Create user.
124        $student = self::getDataGenerator()->create_user();
125
126        // User enrolment.
127        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
128        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
129
130        $this->setCurrentTimeStart();
131        $record = array(
132            'course' => $course->id,
133            'custom' => 0,
134            'feedback' => 1,
135        );
136        $feedback = $this->getDataGenerator()->create_module('feedback', $record);
137        $cm = get_coursemodule_from_instance('feedback', $feedback->id, $course->id);
138        $cm = cm_info::create($cm);
139
140        $this->setUser($student);
141        // Check that upon creation, the updates are only about the new configuration created.
142        $onehourago = time() - HOURSECS;
143        $updates = feedback_check_updates_since($cm, $onehourago);
144        foreach ($updates as $el => $val) {
145            if ($el == 'configuration') {
146                $this->assertTrue($val->updated);
147                $this->assertTimeCurrent($val->timeupdated);
148            } else {
149                $this->assertFalse($val->updated);
150            }
151        }
152
153        $record = [
154            'feedback' => $feedback->id,
155            'userid' => $student->id,
156            'timemodified' => time(),
157            'random_response' => 0,
158            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
159            'courseid' => $course->id,
160        ];
161        $DB->insert_record('feedback_completed', (object)$record);
162        $DB->insert_record('feedback_completedtmp', (object)$record);
163
164        // Check now for finished and unfinished attempts.
165        $updates = feedback_check_updates_since($cm, $onehourago);
166        $this->assertTrue($updates->attemptsunfinished->updated);
167        $this->assertCount(1, $updates->attemptsunfinished->itemids);
168
169        $this->assertTrue($updates->attemptsfinished->updated);
170        $this->assertCount(1, $updates->attemptsfinished->itemids);
171    }
172
173    /**
174     * Test calendar event provide action open.
175     */
176    public function test_feedback_core_calendar_provide_event_action_open() {
177        $this->resetAfterTest();
178        $this->setAdminUser();
179
180        $now = time();
181        $course = $this->getDataGenerator()->create_course();
182        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id,
183                'timeopen' => $now - DAYSECS, 'timeclose' => $now + DAYSECS]);
184        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
185
186        $factory = new \core_calendar\action_factory();
187        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
188
189        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
190        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
191        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
192        $this->assertEquals(1, $actionevent->get_item_count());
193        $this->assertTrue($actionevent->is_actionable());
194    }
195
196    /**
197     * Test calendar event provide action open, viewed by a different user.
198     */
199    public function test_feedback_core_calendar_provide_event_action_open_for_user() {
200        global $DB;
201
202        $this->resetAfterTest();
203        $this->setAdminUser();
204
205        $now = time();
206        $user = $this->getDataGenerator()->create_user();
207        $user2 = $this->getDataGenerator()->create_user();
208        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
209        $course = $this->getDataGenerator()->create_course();
210        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
211
212        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id,
213            'timeopen' => $now - DAYSECS, 'timeclose' => $now + DAYSECS]);
214        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
215        $factory = new \core_calendar\action_factory();
216
217        $this->setUser($user2);
218
219        // User2 checking their events.
220        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
221        $this->assertNull($actionevent);
222
223        // User2 checking $user's events.
224        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
225        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
226        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
227        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
228        $this->assertEquals(1, $actionevent->get_item_count());
229        $this->assertTrue($actionevent->is_actionable());
230    }
231
232    /**
233     * Test calendar event provide action closed.
234     */
235    public function test_feedback_core_calendar_provide_event_action_closed() {
236        $this->resetAfterTest();
237        $this->setAdminUser();
238
239        $course = $this->getDataGenerator()->create_course();
240        $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id,
241                'timeclose' => time() - DAYSECS));
242        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
243
244        $factory = new \core_calendar\action_factory();
245        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
246
247        // No event on the dashboard if feedback is closed.
248        $this->assertNull($actionevent);
249    }
250
251    /**
252     * Test calendar event provide action closed, viewed by a different user.
253     */
254    public function test_feedback_core_calendar_provide_event_action_closed_for_user() {
255        global $DB;
256
257        $this->resetAfterTest();
258        $this->setAdminUser();
259
260        $course = $this->getDataGenerator()->create_course();
261        $user = $this->getDataGenerator()->create_user();
262        $user2 = $this->getDataGenerator()->create_user();
263        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
264        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
265
266        $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id,
267            'timeclose' => time() - DAYSECS));
268        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
269        $factory = new \core_calendar\action_factory();
270        $this->setUser($user2);
271
272        // User2 checking their events.
273        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
274        $this->assertNull($actionevent);
275
276        // User2 checking $user's events.
277        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
278
279        // No event on the dashboard if feedback is closed.
280        $this->assertNull($actionevent);
281    }
282
283    /**
284     * Test calendar event action open in future.
285     *
286     * @throws coding_exception
287     */
288    public function test_feedback_core_calendar_provide_event_action_open_in_future() {
289        $this->resetAfterTest();
290        $this->setAdminUser();
291
292        $course = $this->getDataGenerator()->create_course();
293        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id,
294                'timeopen' => time() + DAYSECS]);
295        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
296
297        $factory = new \core_calendar\action_factory();
298        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
299
300        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
301        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
302        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
303        $this->assertEquals(1, $actionevent->get_item_count());
304        $this->assertFalse($actionevent->is_actionable());
305    }
306
307    /**
308     * Test calendar event action open in future, viewed by a different user.
309     *
310     * @throws coding_exception
311     */
312    public function test_feedback_core_calendar_provide_event_action_open_in_future_for_user() {
313        global $DB;
314
315        $this->resetAfterTest();
316        $this->setAdminUser();
317
318        $course = $this->getDataGenerator()->create_course();
319        $user = $this->getDataGenerator()->create_user();
320        $user2 = $this->getDataGenerator()->create_user();
321        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
322        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
323
324        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id,
325            'timeopen' => time() + DAYSECS]);
326        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
327
328        $factory = new \core_calendar\action_factory();
329        $this->setUser($user2);
330
331        // User2 checking their events.
332        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
333        $this->assertNull($actionevent);
334
335        // User2 checking $user's events.
336        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
337
338        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
339        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
340        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
341        $this->assertEquals(1, $actionevent->get_item_count());
342        $this->assertFalse($actionevent->is_actionable());
343    }
344
345    /**
346     * Test calendar event with no time specified.
347     *
348     * @throws coding_exception
349     */
350    public function test_feedback_core_calendar_provide_event_action_no_time_specified() {
351        $this->resetAfterTest();
352        $this->setAdminUser();
353
354        $course = $this->getDataGenerator()->create_course();
355        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
356        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
357
358        $factory = new \core_calendar\action_factory();
359        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
360
361        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
362        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
363        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
364        $this->assertEquals(1, $actionevent->get_item_count());
365        $this->assertTrue($actionevent->is_actionable());
366    }
367
368    /**
369     * Test calendar event with no time specified, viewed by a different user.
370     *
371     * @throws coding_exception
372     */
373    public function test_feedback_core_calendar_provide_event_action_no_time_specified_for_user() {
374        global $DB;
375
376        $this->resetAfterTest();
377        $this->setAdminUser();
378
379        $course = $this->getDataGenerator()->create_course();
380        $user = $this->getDataGenerator()->create_user();
381        $user2 = $this->getDataGenerator()->create_user();
382        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
383        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
384
385        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
386        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
387
388        $factory = new \core_calendar\action_factory();
389        $this->setUser($user2);
390
391        // User2 checking their events.
392        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
393        $this->assertNull($actionevent);
394
395        // User2 checking $user's events.
396        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
397
398        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
399        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
400        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
401        $this->assertEquals(1, $actionevent->get_item_count());
402        $this->assertTrue($actionevent->is_actionable());
403    }
404
405    /**
406     * A user that can not submit feedback should not have an action.
407     */
408    public function test_feedback_core_calendar_provide_event_action_can_not_submit() {
409        global $DB;
410
411        $this->resetAfterTest();
412        $this->setAdminUser();
413
414        $user = $this->getDataGenerator()->create_user();
415        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
416        $course = $this->getDataGenerator()->create_course();
417        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
418        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
419        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
420        $context = context_module::instance($cm->id);
421        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
422
423        $this->setUser($user);
424        assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $context);
425
426        $factory = new \core_calendar\action_factory();
427        $action = mod_feedback_core_calendar_provide_event_action($event, $factory);
428
429        $this->assertNull($action);
430    }
431
432    /**
433     * A user that can not submit feedback should not have an action, viewed by a different user.
434     */
435    public function test_feedback_core_calendar_provide_event_action_can_not_submit_for_user() {
436        global $DB;
437
438        $this->resetAfterTest();
439        $this->setAdminUser();
440
441        $user = $this->getDataGenerator()->create_user();
442        $user2 = $this->getDataGenerator()->create_user();
443        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
444        $course = $this->getDataGenerator()->create_course();
445        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
446        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
447        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
448        $context = context_module::instance($cm->id);
449        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
450
451        assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $context);
452        $factory = new \core_calendar\action_factory();
453        $this->setUser($user2);
454
455        // User2 checking their events.
456
457        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
458        $this->assertNull($actionevent);
459
460        // User2 checking $user's events.
461        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
462
463        $this->assertNull($actionevent);
464    }
465
466    /**
467     * A user that has already submitted feedback should not have an action.
468     */
469    public function test_feedback_core_calendar_provide_event_action_already_submitted() {
470        global $DB;
471
472        $this->resetAfterTest();
473        $this->setAdminUser();
474
475        $user = $this->getDataGenerator()->create_user();
476        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
477        $course = $this->getDataGenerator()->create_course();
478        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
479        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
480        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
481        $context = context_module::instance($cm->id);
482
483        $this->setUser($user);
484
485        $record = [
486            'feedback' => $feedback->id,
487            'userid' => $user->id,
488            'timemodified' => time(),
489            'random_response' => 0,
490            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
491            'courseid' => 0,
492        ];
493        $DB->insert_record('feedback_completed', (object) $record);
494
495        $factory = new \core_calendar\action_factory();
496        $action = mod_feedback_core_calendar_provide_event_action($event, $factory);
497
498        $this->assertNull($action);
499    }
500
501    /**
502     * A user that has already submitted feedback should not have an action, viewed by a different user.
503     */
504    public function test_feedback_core_calendar_provide_event_action_already_submitted_for_user() {
505        global $DB;
506
507        $this->resetAfterTest();
508        $this->setAdminUser();
509
510        $user = $this->getDataGenerator()->create_user();
511        $user2 = $this->getDataGenerator()->create_user();
512        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
513        $course = $this->getDataGenerator()->create_course();
514        $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]);
515        $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN);
516        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
517        $context = context_module::instance($cm->id);
518
519        $this->setUser($user);
520
521        $record = [
522            'feedback' => $feedback->id,
523            'userid' => $user->id,
524            'timemodified' => time(),
525            'random_response' => 0,
526            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
527            'courseid' => 0,
528        ];
529        $DB->insert_record('feedback_completed', (object) $record);
530
531        $factory = new \core_calendar\action_factory();
532        $this->setUser($user2);
533
534        // User2 checking their events.
535        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id);
536        $this->assertNull($actionevent);
537
538        // User2 checking $user's events.
539        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id);
540
541        $this->assertNull($actionevent);
542    }
543
544    public function test_feedback_core_calendar_provide_event_action_already_completed() {
545        $this->resetAfterTest();
546        set_config('enablecompletion', 1);
547        $this->setAdminUser();
548
549        // Create the activity.
550        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
551        $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id),
552            array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
553
554        // Get some additional data.
555        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
556
557        // Create a calendar event.
558        $event = $this->create_action_event($course->id, $feedback->id,
559            \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
560
561        // Mark the activity as completed.
562        $completion = new completion_info($course);
563        $completion->set_module_viewed($cm);
564
565        // Create an action factory.
566        $factory = new \core_calendar\action_factory();
567
568        // Decorate action event.
569        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
570
571        // Ensure result was null.
572        $this->assertNull($actionevent);
573    }
574
575    public function test_feedback_core_calendar_provide_event_action_already_completed_for_user() {
576        $this->resetAfterTest();
577        set_config('enablecompletion', 1);
578        $this->setAdminUser();
579
580        // Create the activity.
581        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
582        $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id),
583            array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
584
585        // Enrol a student in the course.
586        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
587
588        // Get some additional data.
589        $cm = get_coursemodule_from_instance('feedback', $feedback->id);
590
591        // Create a calendar event.
592        $event = $this->create_action_event($course->id, $feedback->id,
593            \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
594
595        // Mark the activity as completed for the student.
596        $completion = new completion_info($course);
597        $completion->set_module_viewed($cm, $student->id);
598
599        // Create an action factory.
600        $factory = new \core_calendar\action_factory();
601
602        // Decorate action event for the student.
603        $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $student->id);
604
605        // Ensure result was null.
606        $this->assertNull($actionevent);
607    }
608
609    /**
610     * Creates an action event.
611     *
612     * @param int $courseid The course id.
613     * @param int $instanceid The feedback id.
614     * @param string $eventtype The event type. eg. FEEDBACK_EVENT_TYPE_OPEN.
615     * @return bool|calendar_event
616     */
617    private function create_action_event($courseid, $instanceid, $eventtype) {
618        $event = new stdClass();
619        $event->name = 'Calendar event';
620        $event->modulename = 'feedback';
621        $event->courseid = $courseid;
622        $event->instance = $instanceid;
623        $event->type = CALENDAR_EVENT_TYPE_ACTION;
624        $event->eventtype = $eventtype;
625        $event->timestart = time();
626
627        return calendar_event::create($event);
628    }
629
630    /**
631     * Test the callback responsible for returning the completion rule descriptions.
632     * This function should work given either an instance of the module (cm_info), such as when checking the active rules,
633     * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
634     */
635    public function test_mod_feedback_completion_get_active_rule_descriptions() {
636        $this->resetAfterTest();
637        $this->setAdminUser();
638
639        // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
640        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]);
641        $feedback1 = $this->getDataGenerator()->create_module('feedback', [
642            'course' => $course->id,
643            'completion' => 2,
644            'completionsubmit' => 1
645        ]);
646        $feedback2 = $this->getDataGenerator()->create_module('feedback', [
647            'course' => $course->id,
648            'completion' => 2,
649            'completionsubmit' => 0
650        ]);
651        $cm1 = cm_info::create(get_coursemodule_from_instance('feedback', $feedback1->id));
652        $cm2 = cm_info::create(get_coursemodule_from_instance('feedback', $feedback2->id));
653
654        // Data for the stdClass input type.
655        // This type of input would occur when checking the default completion rules for an activity type, where we don't have
656        // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
657        $moddefaults = new stdClass();
658        $moddefaults->customdata = ['customcompletionrules' => ['completionsubmit' => 1]];
659        $moddefaults->completion = 2;
660
661        $activeruledescriptions = [get_string('completionsubmit', 'feedback')];
662        $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
663        $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($cm2), []);
664        $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
665        $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions(new stdClass()), []);
666    }
667
668    /**
669     * An unknown event should not have min or max restrictions.
670     */
671    public function test_get_valid_event_timestart_range_unknown_event() {
672        global $CFG, $DB;
673        require_once($CFG->dirroot . "/calendar/lib.php");
674
675        $this->resetAfterTest(true);
676        $this->setAdminUser();
677        $generator = $this->getDataGenerator();
678        $course = $generator->create_course();
679        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
680        $timeopen = time();
681        $timeclose = $timeopen + DAYSECS;
682        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
683        $feedback->timeopen = $timeopen;
684        $feedback->timeclose = $timeclose;
685        $DB->update_record('feedback', $feedback);
686
687        $event = new \calendar_event([
688            'name' => 'Test event',
689            'description' => '',
690            'format' => 1,
691            'courseid' => $course->id,
692            'groupid' => 0,
693            'userid' => 2,
694            'modulename' => 'feedback',
695            'instance' => $feedback->id,
696            'eventtype' => 'SOME UNKNOWN EVENT',
697            'timestart' => $timeopen,
698            'timeduration' => 86400,
699            'visible' => 1
700        ]);
701
702        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback);
703        $this->assertNull($min);
704        $this->assertNull($max);
705    }
706
707    /**
708     * A FEEDBACK_EVENT_TYPE_OPEN should have a max timestart equal to the activity
709     * close time.
710     */
711    public function test_get_valid_event_timestart_range_event_type_open() {
712        global $CFG, $DB;
713        require_once($CFG->dirroot . "/calendar/lib.php");
714
715        $this->resetAfterTest(true);
716        $this->setAdminUser();
717        $generator = $this->getDataGenerator();
718        $course = $generator->create_course();
719        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
720        $timeopen = time();
721        $timeclose = $timeopen + DAYSECS;
722        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
723        $feedback->timeopen = $timeopen;
724        $feedback->timeclose = $timeclose;
725        $DB->update_record('feedback', $feedback);
726
727        $event = new \calendar_event([
728            'name' => 'Test event',
729            'description' => '',
730            'format' => 1,
731            'courseid' => $course->id,
732            'groupid' => 0,
733            'userid' => 2,
734            'modulename' => 'feedback',
735            'instance' => $feedback->id,
736            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
737            'timestart' => $timeopen,
738            'timeduration' => 86400,
739            'visible' => 1
740        ]);
741
742        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback);
743        $this->assertNull($min);
744        $this->assertEquals($timeclose, $max[0]);
745        $this->assertNotEmpty($max[1]);
746    }
747
748    /**
749     * A FEEDBACK_EVENT_TYPE_OPEN should not have a max timestamp if the activity
750     * doesn't have a close date.
751     */
752    public function test_get_valid_event_timestart_range_event_type_open_no_close() {
753        global $CFG, $DB;
754        require_once($CFG->dirroot . "/calendar/lib.php");
755
756        $this->resetAfterTest(true);
757        $this->setAdminUser();
758        $generator = $this->getDataGenerator();
759        $course = $generator->create_course();
760        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
761        $timeopen = time();
762        $timeclose = $timeopen + DAYSECS;
763        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
764        $feedback->timeopen = $timeopen;
765        $feedback->timeclose = 0;
766        $DB->update_record('feedback', $feedback);
767
768        $event = new \calendar_event([
769            'name' => 'Test event',
770            'description' => '',
771            'format' => 1,
772            'courseid' => $course->id,
773            'groupid' => 0,
774            'userid' => 2,
775            'modulename' => 'feedback',
776            'instance' => $feedback->id,
777            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
778            'timestart' => $timeopen,
779            'timeduration' => 86400,
780            'visible' => 1
781        ]);
782
783        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback);
784        $this->assertNull($min);
785        $this->assertNull($max);
786    }
787
788    /**
789     * A FEEDBACK_EVENT_TYPE_CLOSE should have a min timestart equal to the activity
790     * open time.
791     */
792    public function test_get_valid_event_timestart_range_event_type_close() {
793        global $CFG, $DB;
794        require_once($CFG->dirroot . "/calendar/lib.php");
795
796        $this->resetAfterTest(true);
797        $this->setAdminUser();
798        $generator = $this->getDataGenerator();
799        $course = $generator->create_course();
800        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
801        $timeopen = time();
802        $timeclose = $timeopen + DAYSECS;
803        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
804        $feedback->timeopen = $timeopen;
805        $feedback->timeclose = $timeclose;
806        $DB->update_record('feedback', $feedback);
807
808        $event = new \calendar_event([
809            'name' => 'Test event',
810            'description' => '',
811            'format' => 1,
812            'courseid' => $course->id,
813            'groupid' => 0,
814            'userid' => 2,
815            'modulename' => 'feedback',
816            'instance' => $feedback->id,
817            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
818            'timestart' => $timeopen,
819            'timeduration' => 86400,
820            'visible' => 1
821        ]);
822
823        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback);
824        $this->assertEquals($timeopen, $min[0]);
825        $this->assertNotEmpty($min[1]);
826        $this->assertNull($max);
827    }
828
829    /**
830     * A FEEDBACK_EVENT_TYPE_CLOSE should not have a minimum timestamp if the activity
831     * doesn't have an open date.
832     */
833    public function test_get_valid_event_timestart_range_event_type_close_no_open() {
834        global $CFG, $DB;
835        require_once($CFG->dirroot . "/calendar/lib.php");
836
837        $this->resetAfterTest(true);
838        $this->setAdminUser();
839        $generator = $this->getDataGenerator();
840        $course = $generator->create_course();
841        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
842        $timeopen = time();
843        $timeclose = $timeopen + DAYSECS;
844        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
845        $feedback->timeopen = 0;
846        $feedback->timeclose = $timeclose;
847        $DB->update_record('feedback', $feedback);
848
849        $event = new \calendar_event([
850            'name' => 'Test event',
851            'description' => '',
852            'format' => 1,
853            'courseid' => $course->id,
854            'groupid' => 0,
855            'userid' => 2,
856            'modulename' => 'feedback',
857            'instance' => $feedback->id,
858            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
859            'timestart' => $timeopen,
860            'timeduration' => 86400,
861            'visible' => 1
862        ]);
863
864        list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback);
865        $this->assertNull($min);
866        $this->assertNull($max);
867    }
868
869    /**
870     * An unkown event type should not change the feedback instance.
871     */
872    public function test_mod_feedback_core_calendar_event_timestart_updated_unknown_event() {
873        global $CFG, $DB;
874        require_once($CFG->dirroot . "/calendar/lib.php");
875
876        $this->resetAfterTest(true);
877        $this->setAdminUser();
878        $generator = $this->getDataGenerator();
879        $course = $generator->create_course();
880        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
881        $timeopen = time();
882        $timeclose = $timeopen + DAYSECS;
883        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
884        $feedback->timeopen = $timeopen;
885        $feedback->timeclose = $timeclose;
886        $DB->update_record('feedback', $feedback);
887
888        // Create a valid event.
889        $event = new \calendar_event([
890            'name' => 'Test event',
891            'description' => '',
892            'format' => 1,
893            'courseid' => $course->id,
894            'groupid' => 0,
895            'userid' => 2,
896            'modulename' => 'feedback',
897            'instance' => $feedback->id,
898            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN . "SOMETHING ELSE",
899            'timestart' => 1,
900            'timeduration' => 86400,
901            'visible' => 1
902        ]);
903
904        mod_feedback_core_calendar_event_timestart_updated($event, $feedback);
905
906        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
907        $this->assertEquals($timeopen, $feedback->timeopen);
908        $this->assertEquals($timeclose, $feedback->timeclose);
909    }
910
911    /**
912     * A FEEDBACK_EVENT_TYPE_OPEN event should update the timeopen property of
913     * the feedback activity.
914     */
915    public function test_mod_feedback_core_calendar_event_timestart_updated_open_event() {
916        global $CFG, $DB;
917        require_once($CFG->dirroot . "/calendar/lib.php");
918
919        $this->resetAfterTest(true);
920        $this->setAdminUser();
921        $generator = $this->getDataGenerator();
922        $course = $generator->create_course();
923        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
924        $timeopen = time();
925        $timeclose = $timeopen + DAYSECS;
926        $timemodified = 1;
927        $newtimeopen = $timeopen - DAYSECS;
928        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
929        $feedback->timeopen = $timeopen;
930        $feedback->timeclose = $timeclose;
931        $feedback->timemodified = $timemodified;
932        $DB->update_record('feedback', $feedback);
933
934        // Create a valid event.
935        $event = new \calendar_event([
936            'name' => 'Test event',
937            'description' => '',
938            'format' => 1,
939            'courseid' => $course->id,
940            'groupid' => 0,
941            'userid' => 2,
942            'modulename' => 'feedback',
943            'instance' => $feedback->id,
944            'eventtype' => FEEDBACK_EVENT_TYPE_OPEN,
945            'timestart' => $newtimeopen,
946            'timeduration' => 86400,
947            'visible' => 1
948        ]);
949
950        mod_feedback_core_calendar_event_timestart_updated($event, $feedback);
951
952        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
953        // Ensure the timeopen property matches the event timestart.
954        $this->assertEquals($newtimeopen, $feedback->timeopen);
955        // Ensure the timeclose isn't changed.
956        $this->assertEquals($timeclose, $feedback->timeclose);
957        // Ensure the timemodified property has been changed.
958        $this->assertNotEquals($timemodified, $feedback->timemodified);
959    }
960
961    /**
962     * A FEEDBACK_EVENT_TYPE_CLOSE event should update the timeclose property of
963     * the feedback activity.
964     */
965    public function test_mod_feedback_core_calendar_event_timestart_updated_close_event() {
966        global $CFG, $DB;
967        require_once($CFG->dirroot . "/calendar/lib.php");
968
969        $this->resetAfterTest(true);
970        $this->setAdminUser();
971        $generator = $this->getDataGenerator();
972        $course = $generator->create_course();
973        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
974        $timeopen = time();
975        $timeclose = $timeopen + DAYSECS;
976        $timemodified = 1;
977        $newtimeclose = $timeclose + DAYSECS;
978        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
979        $feedback->timeopen = $timeopen;
980        $feedback->timeclose = $timeclose;
981        $feedback->timemodified = $timemodified;
982        $DB->update_record('feedback', $feedback);
983
984        // Create a valid event.
985        $event = new \calendar_event([
986            'name' => 'Test event',
987            'description' => '',
988            'format' => 1,
989            'courseid' => $course->id,
990            'groupid' => 0,
991            'userid' => 2,
992            'modulename' => 'feedback',
993            'instance' => $feedback->id,
994            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
995            'timestart' => $newtimeclose,
996            'timeduration' => 86400,
997            'visible' => 1
998        ]);
999
1000        mod_feedback_core_calendar_event_timestart_updated($event, $feedback);
1001
1002        $feedback = $DB->get_record('feedback', ['id' => $feedback->id]);
1003        // Ensure the timeclose property matches the event timestart.
1004        $this->assertEquals($newtimeclose, $feedback->timeclose);
1005        // Ensure the timeopen isn't changed.
1006        $this->assertEquals($timeopen, $feedback->timeopen);
1007        // Ensure the timemodified property has been changed.
1008        $this->assertNotEquals($timemodified, $feedback->timemodified);
1009    }
1010
1011    /**
1012     * If a student somehow finds a way to update the calendar event
1013     * then the callback should not be executed to update the activity
1014     * properties as well because that would be a security issue.
1015     */
1016    public function test_student_role_cant_update_time_close_event() {
1017        global $CFG, $DB;
1018        require_once($CFG->dirroot . '/calendar/lib.php');
1019
1020        $this->resetAfterTest();
1021        $this->setAdminUser();
1022
1023        $generator = $this->getDataGenerator();
1024        $user = $generator->create_user();
1025        $course = $generator->create_course();
1026        $context = context_course::instance($course->id);
1027        $roleid = $generator->create_role();
1028        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
1029        $timeopen = time();
1030        $timeclose = $timeopen + DAYSECS;
1031        $timemodified = 1;
1032        $newtimeclose = $timeclose + DAYSECS;
1033        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
1034        $feedback->timeopen = $timeopen;
1035        $feedback->timeclose = $timeclose;
1036        $feedback->timemodified = $timemodified;
1037        $DB->update_record('feedback', $feedback);
1038
1039        $generator->enrol_user($user->id, $course->id, 'student');
1040        $generator->role_assign($roleid, $user->id, $context->id);
1041
1042        // Create a valid event.
1043        $event = new \calendar_event([
1044            'name' => 'Test event',
1045            'description' => '',
1046            'format' => 1,
1047            'courseid' => $course->id,
1048            'groupid' => 0,
1049            'userid' => $user->id,
1050            'modulename' => 'feedback',
1051            'instance' => $feedback->id,
1052            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
1053            'timestart' => $newtimeclose,
1054            'timeduration' => 86400,
1055            'visible' => 1
1056        ]);
1057
1058        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1059        assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true);
1060
1061        $this->setUser($user);
1062
1063        mod_feedback_core_calendar_event_timestart_updated($event, $feedback);
1064
1065        $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]);
1066        // The activity shouldn't have been updated because the user
1067        // doesn't have permissions to do it.
1068        $this->assertEquals($timeclose, $newfeedback->timeclose);
1069    }
1070
1071    /**
1072     * The activity should update if a teacher modifies the calendar
1073     * event.
1074     */
1075    public function test_teacher_role_can_update_time_close_event() {
1076        global $CFG, $DB;
1077        require_once($CFG->dirroot . '/calendar/lib.php');
1078
1079        $this->resetAfterTest();
1080        $this->setAdminUser();
1081
1082        $generator = $this->getDataGenerator();
1083        $user = $generator->create_user();
1084        $course = $generator->create_course();
1085        $context = context_course::instance($course->id);
1086        $roleid = $generator->create_role();
1087        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
1088        $timeopen = time();
1089        $timeclose = $timeopen + DAYSECS;
1090        $timemodified = 1;
1091        $newtimeclose = $timeclose + DAYSECS;
1092        $feedback = $feedbackgenerator->create_instance(['course' => $course->id]);
1093        $feedback->timeopen = $timeopen;
1094        $feedback->timeclose = $timeclose;
1095        $feedback->timemodified = $timemodified;
1096        $DB->update_record('feedback', $feedback);
1097
1098        $generator->enrol_user($user->id, $course->id, 'teacher');
1099        $generator->role_assign($roleid, $user->id, $context->id);
1100
1101        // Create a valid event.
1102        $event = new \calendar_event([
1103            'name' => 'Test event',
1104            'description' => '',
1105            'format' => 1,
1106            'courseid' => $course->id,
1107            'groupid' => 0,
1108            'userid' => $user->id,
1109            'modulename' => 'feedback',
1110            'instance' => $feedback->id,
1111            'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE,
1112            'timestart' => $newtimeclose,
1113            'timeduration' => 86400,
1114            'visible' => 1
1115        ]);
1116
1117        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
1118        assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true);
1119
1120        $this->setUser($user);
1121
1122        $sink = $this->redirectEvents();
1123
1124        mod_feedback_core_calendar_event_timestart_updated($event, $feedback);
1125
1126        $triggeredevents = $sink->get_events();
1127        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
1128            return is_a($e, 'core\event\course_module_updated');
1129        });
1130
1131        $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]);
1132        // The activity should have been updated because the user
1133        // has permissions to do it.
1134        $this->assertEquals($newtimeclose, $newfeedback->timeclose);
1135        // A course_module_updated event should be fired if the module
1136        // was successfully modified.
1137        $this->assertNotEmpty($moduleupdatedevents);
1138    }
1139
1140    /**
1141     * A user who does not have capabilities to add events to the calendar should be able to create an feedback.
1142     */
1143    public function test_creation_with_no_calendar_capabilities() {
1144        $this->resetAfterTest();
1145        $course = self::getDataGenerator()->create_course();
1146        $context = context_course::instance($course->id);
1147        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
1148        $roleid = self::getDataGenerator()->create_role();
1149        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
1150        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
1151        $generator = self::getDataGenerator()->get_plugin_generator('mod_feedback');
1152        // Create an instance as a user without the calendar capabilities.
1153        $this->setUser($user);
1154        $time = time();
1155        $params = array(
1156            'course' => $course->id,
1157            'timeopen' => $time + 200,
1158            'timeclose' => $time + 2000,
1159        );
1160        $generator->create_instance($params);
1161    }
1162}
1163