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/**
18 * Feedback module external functions tests
19 *
20 * @package    mod_feedback
21 * @category   external
22 * @copyright  2017 Juan Leyva <juan@moodle.com>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @since      Moodle 3.3
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30
31require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32require_once($CFG->dirroot . '/mod/feedback/lib.php');
33
34use mod_feedback\external\feedback_summary_exporter;
35
36/**
37 * Feedback module external functions tests
38 *
39 * @package    mod_feedback
40 * @category   external
41 * @copyright  2017 Juan Leyva <juan@moodle.com>
42 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 * @since      Moodle 3.3
44 */
45class mod_feedback_external_testcase extends externallib_advanced_testcase {
46
47    /**
48     * Set up for every test
49     */
50    public function setUp() {
51        global $DB;
52        $this->resetAfterTest();
53        $this->setAdminUser();
54
55        // Setup test data.
56        $this->course = $this->getDataGenerator()->create_course();
57        $this->feedback = $this->getDataGenerator()->create_module('feedback',
58            array('course' => $this->course->id, 'email_notification' => 1));
59        $this->context = context_module::instance($this->feedback->cmid);
60        $this->cm = get_coursemodule_from_instance('feedback', $this->feedback->id);
61
62        // Create users.
63        $this->student = self::getDataGenerator()->create_user();
64        $this->teacher = self::getDataGenerator()->create_user();
65
66        // Users enrolments.
67        $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
68        $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
69        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
70        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
71    }
72
73    /**
74     * Helper method to add items to an existing feedback.
75     *
76     * @param stdClass  $feedback feedback instance
77     * @param integer $pagescount the number of pages we want in the feedback
78     * @return array list of items created
79     */
80    public function populate_feedback($feedback, $pagescount = 1) {
81        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
82        $itemscreated = [];
83
84        // Create at least one page.
85        $itemscreated[] = $feedbackgenerator->create_item_label($feedback);
86        $itemscreated[] = $feedbackgenerator->create_item_info($feedback);
87        $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback);
88
89        // Check if we want more pages.
90        for ($i = 1; $i < $pagescount; $i++) {
91            $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback);
92            $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback);
93            $itemscreated[] = $feedbackgenerator->create_item_multichoicerated($feedback);
94            $itemscreated[] = $feedbackgenerator->create_item_textarea($feedback);
95            $itemscreated[] = $feedbackgenerator->create_item_textfield($feedback);
96            $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback);
97        }
98        return $itemscreated;
99    }
100
101
102    /**
103     * Test test_mod_feedback_get_feedbacks_by_courses
104     */
105    public function test_mod_feedback_get_feedbacks_by_courses() {
106        global $DB;
107
108        // Create additional course.
109        $course2 = self::getDataGenerator()->create_course();
110
111        // Second feedback.
112        $record = new stdClass();
113        $record->course = $course2->id;
114        $feedback2 = self::getDataGenerator()->create_module('feedback', $record);
115
116        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
117        $enrol = enrol_get_plugin('manual');
118        $enrolinstances = enrol_get_instances($course2->id, true);
119        foreach ($enrolinstances as $courseenrolinstance) {
120            if ($courseenrolinstance->enrol == "manual") {
121                $instance2 = $courseenrolinstance;
122                break;
123            }
124        }
125        $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
126
127        self::setUser($this->student);
128
129        $returndescription = mod_feedback_external::get_feedbacks_by_courses_returns();
130
131        // Create what we expect to be returned when querying the two courses.
132        // First for the student user.
133        $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'anonymous',
134            'multiple_submit', 'autonumbering', 'page_after_submitformat', 'publish_stats', 'completionsubmit');
135
136        $properties = feedback_summary_exporter::read_properties_definition();
137
138        // Add expected coursemodule and data.
139        $feedback1 = $this->feedback;
140        $feedback1->coursemodule = $feedback1->cmid;
141        $feedback1->introformat = 1;
142        $feedback1->introfiles = [];
143
144        $feedback2->coursemodule = $feedback2->cmid;
145        $feedback2->introformat = 1;
146        $feedback2->introfiles = [];
147
148        foreach ($expectedfields as $field) {
149            if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) {
150                $feedback1->{$field} = (bool) $feedback1->{$field};
151                $feedback2->{$field} = (bool) $feedback2->{$field};
152            }
153            $expected1[$field] = $feedback1->{$field};
154            $expected2[$field] = $feedback2->{$field};
155        }
156
157        $expectedfeedbacks = array($expected2, $expected1);
158
159        // Call the external function passing course ids.
160        $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id, $this->course->id));
161        $result = external_api::clean_returnvalue($returndescription, $result);
162
163        $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
164        $this->assertCount(0, $result['warnings']);
165
166        // Call the external function without passing course id.
167        $result = mod_feedback_external::get_feedbacks_by_courses();
168        $result = external_api::clean_returnvalue($returndescription, $result);
169        $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
170        $this->assertCount(0, $result['warnings']);
171
172        // Unenrol user from second course and alter expected feedbacks.
173        $enrol->unenrol_user($instance2, $this->student->id);
174        array_shift($expectedfeedbacks);
175
176        // Call the external function without passing course id.
177        $result = mod_feedback_external::get_feedbacks_by_courses();
178        $result = external_api::clean_returnvalue($returndescription, $result);
179        $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
180
181        // Call for the second course we unenrolled the user from, expected warning.
182        $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id));
183        $this->assertCount(1, $result['warnings']);
184        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
185        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
186
187        // Now, try as a teacher for getting all the additional fields.
188        self::setUser($this->teacher);
189
190        $additionalfields = array('email_notification', 'site_after_submit', 'page_after_submit', 'timeopen', 'timeclose',
191            'timemodified', 'pageaftersubmitfiles');
192
193        $feedback1->pageaftersubmitfiles = [];
194
195        foreach ($additionalfields as $field) {
196            if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) {
197                $feedback1->{$field} = (bool) $feedback1->{$field};
198            }
199            $expectedfeedbacks[0][$field] = $feedback1->{$field};
200        }
201        $expectedfeedbacks[0]['page_after_submitformat'] = 1;
202
203        $result = mod_feedback_external::get_feedbacks_by_courses();
204        $result = external_api::clean_returnvalue($returndescription, $result);
205        $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
206
207        // Admin also should get all the information.
208        self::setAdminUser();
209
210        $result = mod_feedback_external::get_feedbacks_by_courses(array($this->course->id));
211        $result = external_api::clean_returnvalue($returndescription, $result);
212        $this->assertEquals($expectedfeedbacks, $result['feedbacks']);
213    }
214
215    /**
216     * Test get_feedback_access_information function with basic defaults for student.
217     */
218    public function test_get_feedback_access_information_student() {
219
220        self::setUser($this->student);
221        $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
222        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
223
224        $this->assertFalse($result['canviewanalysis']);
225        $this->assertFalse($result['candeletesubmissions']);
226        $this->assertFalse($result['canviewreports']);
227        $this->assertFalse($result['canedititems']);
228        $this->assertTrue($result['cancomplete']);
229        $this->assertTrue($result['cansubmit']);
230        $this->assertTrue($result['isempty']);
231        $this->assertTrue($result['isopen']);
232        $this->assertTrue($result['isanonymous']);
233        $this->assertFalse($result['isalreadysubmitted']);
234    }
235
236    /**
237     * Test get_feedback_access_information function with basic defaults for teacher.
238     */
239    public function test_get_feedback_access_information_teacher() {
240
241        self::setUser($this->teacher);
242        $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
243        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
244
245        $this->assertTrue($result['canviewanalysis']);
246        $this->assertTrue($result['canviewreports']);
247        $this->assertTrue($result['canedititems']);
248        $this->assertTrue($result['candeletesubmissions']);
249        $this->assertFalse($result['cancomplete']);
250        $this->assertTrue($result['cansubmit']);
251        $this->assertTrue($result['isempty']);
252        $this->assertTrue($result['isopen']);
253        $this->assertTrue($result['isanonymous']);
254        $this->assertFalse($result['isalreadysubmitted']);
255
256        // Add some items to the feedback and check is not empty any more.
257        self::populate_feedback($this->feedback);
258        $result = mod_feedback_external::get_feedback_access_information($this->feedback->id);
259        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
260        $this->assertFalse($result['isempty']);
261    }
262
263    /**
264     * Test view_feedback invalid id.
265     */
266    public function test_view_feedback_invalid_id() {
267        // Test invalid instance id.
268        $this->expectException('moodle_exception');
269        mod_feedback_external::view_feedback(0);
270    }
271    /**
272     * Test view_feedback not enrolled user.
273     */
274    public function test_view_feedback_not_enrolled_user() {
275        $usernotenrolled = self::getDataGenerator()->create_user();
276        $this->setUser($usernotenrolled);
277        $this->expectException('moodle_exception');
278        mod_feedback_external::view_feedback(0);
279    }
280    /**
281     * Test view_feedback no capabilities.
282     */
283    public function test_view_feedback_no_capabilities() {
284        // Test user with no capabilities.
285        // We need a explicit prohibit since this capability is allowed for students by default.
286        assign_capability('mod/feedback:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
287        accesslib_clear_all_caches_for_unit_testing();
288        $this->expectException('moodle_exception');
289        mod_feedback_external::view_feedback(0);
290    }
291    /**
292     * Test view_feedback.
293     */
294    public function test_view_feedback() {
295        // Test user with full capabilities.
296        $this->setUser($this->student);
297        // Trigger and capture the event.
298        $sink = $this->redirectEvents();
299        $result = mod_feedback_external::view_feedback($this->feedback->id);
300        $result = external_api::clean_returnvalue(mod_feedback_external::view_feedback_returns(), $result);
301        $events = $sink->get_events();
302        $this->assertCount(1, $events);
303        $event = array_shift($events);
304        // Checking that the event contains the expected values.
305        $this->assertInstanceOf('\mod_feedback\event\course_module_viewed', $event);
306        $this->assertEquals($this->context, $event->get_context());
307        $moodledata = new \moodle_url('/mod/feedback/view.php', array('id' => $this->cm->id));
308        $this->assertEquals($moodledata, $event->get_url());
309        $this->assertEventContextNotUsed($event);
310        $this->assertNotEmpty($event->get_name());
311    }
312
313    /**
314     * Test get_current_completed_tmp.
315     */
316    public function test_get_current_completed_tmp() {
317        global $DB;
318
319        // Force non anonymous.
320        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
321        // Add a completed_tmp record.
322        $record = [
323            'feedback' => $this->feedback->id,
324            'userid' => $this->student->id,
325            'guestid' => '',
326            'timemodified' => time() - DAYSECS,
327            'random_response' => 0,
328            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
329            'courseid' => $this->course->id,
330        ];
331        $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record);
332
333        // Test user with full capabilities.
334        $this->setUser($this->student);
335
336        $result = mod_feedback_external::get_current_completed_tmp($this->feedback->id);
337        $result = external_api::clean_returnvalue(mod_feedback_external::get_current_completed_tmp_returns(), $result);
338        $this->assertEquals($record['id'], $result['feedback']['id']);
339    }
340
341    /**
342     * Test get_items.
343     */
344    public function test_get_items() {
345        // Test user with full capabilities.
346        $this->setUser($this->student);
347
348        // Add questions to the feedback, we are adding 2 pages of questions.
349        $itemscreated = self::populate_feedback($this->feedback, 2);
350
351        $result = mod_feedback_external::get_items($this->feedback->id);
352        $result = external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result);
353        $this->assertCount(count($itemscreated), $result['items']);
354        $index = 1;
355        foreach ($result['items'] as $key => $item) {
356            if (is_numeric($itemscreated[$key])) {
357                continue; // Page break.
358            }
359            // Cannot compare directly the exporter and the db object (exporter have more fields).
360            $this->assertEquals($itemscreated[$key]->id, $item['id']);
361            $this->assertEquals($itemscreated[$key]->typ, $item['typ']);
362            $this->assertEquals($itemscreated[$key]->name, $item['name']);
363            $this->assertEquals($itemscreated[$key]->label, $item['label']);
364
365            if ($item['hasvalue']) {
366                $this->assertEquals($index, $item['itemnumber']);
367                $index++;
368            }
369        }
370    }
371
372    /**
373     * Test launch_feedback.
374     */
375    public function test_launch_feedback() {
376        global $DB;
377
378        // Test user with full capabilities.
379        $this->setUser($this->student);
380
381        // Add questions to the feedback, we are adding 2 pages of questions.
382        $itemscreated = self::populate_feedback($this->feedback, 2);
383
384        // First try a feedback we didn't attempt.
385        $result = mod_feedback_external::launch_feedback($this->feedback->id);
386        $result = external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result);
387        $this->assertEquals(0, $result['gopage']);
388
389        // Now, try a feedback that we attempted.
390        // Force non anonymous.
391        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
392        // Add a completed_tmp record.
393        $record = [
394            'feedback' => $this->feedback->id,
395            'userid' => $this->student->id,
396            'guestid' => '',
397            'timemodified' => time() - DAYSECS,
398            'random_response' => 0,
399            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
400            'courseid' => $this->course->id,
401        ];
402        $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record);
403
404        // Add a response to the feedback for each question type with possible values.
405        $response = [
406            'course_id' => $this->course->id,
407            'item' => $itemscreated[1]->id, // First item is the info question.
408            'completed' => $record['id'],
409            'tmp_completed' => $record['id'],
410            'value' => 'A',
411        ];
412        $DB->insert_record('feedback_valuetmp', (object) $response);
413        $response = [
414            'course_id' => $this->course->id,
415            'item' => $itemscreated[2]->id, // Second item is the numeric question.
416            'completed' => $record['id'],
417            'tmp_completed' => $record['id'],
418            'value' => 5,
419        ];
420        $DB->insert_record('feedback_valuetmp', (object) $response);
421
422        $result = mod_feedback_external::launch_feedback($this->feedback->id);
423        $result = external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result);
424        $this->assertEquals(1, $result['gopage']);
425    }
426
427    /**
428     * Test get_page_items.
429     */
430    public function test_get_page_items() {
431        // Test user with full capabilities.
432        $this->setUser($this->student);
433
434        // Add questions to the feedback, we are adding 2 pages of questions.
435        $itemscreated = self::populate_feedback($this->feedback, 2);
436
437        // Retrieve first page.
438        $result = mod_feedback_external::get_page_items($this->feedback->id, 0);
439        $result = external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result);
440        $this->assertCount(3, $result['items']);    // The first page has 3 items.
441        $this->assertTrue($result['hasnextpage']);
442        $this->assertFalse($result['hasprevpage']);
443
444        // Retrieve second page.
445        $result = mod_feedback_external::get_page_items($this->feedback->id, 1);
446        $result = external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result);
447        $this->assertCount(5, $result['items']);    // The second page has 5 items (page break doesn't count).
448        $this->assertFalse($result['hasnextpage']);
449        $this->assertTrue($result['hasprevpage']);
450    }
451
452    /**
453     * Test process_page.
454     */
455    public function test_process_page() {
456        global $DB;
457
458        // Test user with full capabilities.
459        $this->setUser($this->student);
460        $pagecontents = 'You finished it!';
461        $DB->set_field('feedback', 'page_after_submit', $pagecontents, array('id' => $this->feedback->id));
462
463        // Add questions to the feedback, we are adding 2 pages of questions.
464        $itemscreated = self::populate_feedback($this->feedback, 2);
465
466        $data = [];
467        foreach ($itemscreated as $item) {
468
469            if (empty($item->hasvalue)) {
470                continue;
471            }
472
473            switch ($item->typ) {
474                case 'textarea':
475                case 'textfield':
476                    $value = 'Lorem ipsum';
477                    break;
478                case 'numeric':
479                    $value = 5;
480                    break;
481                case 'multichoice':
482                    $value = '1';
483                    break;
484                case 'multichoicerated':
485                    $value = '1';
486                    break;
487                case 'info':
488                    $value = format_string($this->course->shortname, true, array('context' => $this->context));
489                    break;
490                default:
491                    $value = '';
492            }
493            $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value];
494        }
495
496        // Process first page.
497        $firstpagedata = [$data[0], $data[1]];
498        $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata);
499        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
500        $this->assertEquals(1, $result['jumpto']);
501        $this->assertFalse($result['completed']);
502
503        // Now, process the second page. But first we are going back to the first page.
504        $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
505        $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, true);
506        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
507        $this->assertFalse($result['completed']);
508        $this->assertEquals(0, $result['jumpto']);  // We jumped to the first page.
509        // Check the values were correctly saved.
510        $tmpitems = $DB->get_records('feedback_valuetmp');
511        $this->assertCount(7, $tmpitems);   // 2 from the first page + 5 from the second page.
512
513        // Go forward again (sending the same data).
514        $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata);
515        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
516        $this->assertEquals(1, $result['jumpto']);
517        $this->assertFalse($result['completed']);
518        $tmpitems = $DB->get_records('feedback_valuetmp');
519        $this->assertCount(7, $tmpitems);   // 2 from the first page + 5 from the second page.
520
521        // And finally, save everything! We are going to modify one previous recorded value.
522        $messagessink = $this->redirectMessages();
523        $data[2]['value'] = 2; // 2 is value of the option 'b'.
524        $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
525        $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata);
526        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
527        $this->assertTrue($result['completed']);
528        $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false);
529        // Check all the items were saved.
530        $items = $DB->get_records('feedback_value');
531        $this->assertCount(7, $items);
532        // Check if the one we modified was correctly saved.
533        $itemid = $itemscreated[4]->id;
534        $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
535        $mcitem = new feedback_item_multichoice();
536        $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
537        $this->assertEquals('b', $itemval);
538
539        // Check that the answers are saved for course 0.
540        foreach ($items as $item) {
541            $this->assertEquals(0, $item->course_id);
542        }
543        $completed = $DB->get_record('feedback_completed', []);
544        $this->assertEquals(0, $completed->courseid);
545
546        // Test notifications sent.
547        $messages = $messagessink->get_messages();
548        $messagessink->close();
549        // Test customdata.
550        $customdata = json_decode($messages[0]->customdata);
551        $this->assertEquals($this->feedback->id, $customdata->instance);
552        $this->assertEquals($this->feedback->cmid, $customdata->cmid);
553        $this->assertObjectHasAttribute('notificationiconurl', $customdata);
554    }
555
556    /**
557     * Test process_page for a site feedback.
558     */
559    public function test_process_page_site_feedback() {
560        global $DB;
561        $pagecontents = 'You finished it!';
562        $this->feedback = $this->getDataGenerator()->create_module('feedback',
563            array('course' => SITEID, 'page_after_submit' => $pagecontents));
564
565        // Test user with full capabilities.
566        $this->setUser($this->student);
567
568        // Add questions to the feedback, we are adding 2 pages of questions.
569        $itemscreated = self::populate_feedback($this->feedback, 2);
570
571        $data = [];
572        foreach ($itemscreated as $item) {
573
574            if (empty($item->hasvalue)) {
575                continue;
576            }
577
578            switch ($item->typ) {
579                case 'textarea':
580                case 'textfield':
581                    $value = 'Lorem ipsum';
582                    break;
583                case 'numeric':
584                    $value = 5;
585                    break;
586                case 'multichoice':
587                    $value = '1';
588                    break;
589                case 'multichoicerated':
590                    $value = '1';
591                    break;
592                case 'info':
593                    $value = format_string($this->course->shortname, true, array('context' => $this->context));
594                    break;
595                default:
596                    $value = '';
597            }
598            $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value];
599        }
600
601        // Process first page.
602        $firstpagedata = [$data[0], $data[1]];
603        $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata, false, $this->course->id);
604        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
605        $this->assertEquals(1, $result['jumpto']);
606        $this->assertFalse($result['completed']);
607
608        // Process second page.
609        $data[2]['value'] = 2; // 2 is value of the option 'b';
610        $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
611        $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, false, $this->course->id);
612        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
613        $this->assertTrue($result['completed']);
614        $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false);
615        // Check all the items were saved.
616        $items = $DB->get_records('feedback_value');
617        $this->assertCount(7, $items);
618        // Check if the one we modified was correctly saved.
619        $itemid = $itemscreated[4]->id;
620        $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
621        $mcitem = new feedback_item_multichoice();
622        $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
623        $this->assertEquals('b', $itemval);
624
625        // Check that the answers are saved for the correct course.
626        foreach ($items as $item) {
627            $this->assertEquals($this->course->id, $item->course_id);
628        }
629        $completed = $DB->get_record('feedback_completed', []);
630        $this->assertEquals($this->course->id, $completed->courseid);
631    }
632
633    /**
634     * Test get_analysis.
635     */
636    public function test_get_analysis() {
637        // Test user with full capabilities.
638        $this->setUser($this->student);
639
640        // Create a very simple feedback.
641        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
642        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
643        $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
644
645        $pagedata = [
646            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
647            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
648        ];
649        // Process the feedback, there is only one page so the feedback will be completed.
650        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
651        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
652        $this->assertTrue($result['completed']);
653
654        // Retrieve analysis.
655        $this->setUser($this->teacher);
656        $result = mod_feedback_external::get_analysis($this->feedback->id);
657        $result = external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result);
658        $this->assertEquals(1, $result['completedcount']);  // 1 feedback completed.
659        $this->assertEquals(2, $result['itemscount']);  // 2 items in the feedback.
660        $this->assertCount(2, $result['itemsdata']);
661        $this->assertCount(1, $result['itemsdata'][0]['data']); // There are 1 response per item.
662        $this->assertCount(1, $result['itemsdata'][1]['data']);
663        // Check we receive the info the students filled.
664        foreach ($result['itemsdata'] as $data) {
665            if ($data['item']['id'] == $numericitem->id) {
666                $this->assertEquals(5, $data['data'][0]);
667            } else {
668                $this->assertEquals('abc', $data['data'][0]);
669            }
670        }
671
672        // Create another user / response.
673        $anotherstudent = self::getDataGenerator()->create_user();
674        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
675        $this->setUser($anotherstudent);
676
677        // Process the feedback, there is only one page so the feedback will be completed.
678        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
679        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
680        $this->assertTrue($result['completed']);
681
682        // Retrieve analysis.
683        $this->setUser($this->teacher);
684        $result = mod_feedback_external::get_analysis($this->feedback->id);
685        $result = external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result);
686        $this->assertEquals(2, $result['completedcount']);  // 2 feedback completed.
687        $this->assertEquals(2, $result['itemscount']);
688        $this->assertCount(2, $result['itemsdata'][0]['data']); // There are 2 responses per item.
689        $this->assertCount(2, $result['itemsdata'][1]['data']);
690    }
691
692    /**
693     * Test get_unfinished_responses.
694     */
695    public function test_get_unfinished_responses() {
696        // Test user with full capabilities.
697        $this->setUser($this->student);
698
699        // Create a very simple feedback.
700        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
701        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
702        $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
703        $feedbackgenerator->create_item_pagebreak($this->feedback);
704        $labelitem = $feedbackgenerator->create_item_label($this->feedback);
705        $numericitem2 = $feedbackgenerator->create_item_numeric($this->feedback);
706
707        $pagedata = [
708            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
709            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
710        ];
711        // Process the feedback, there are two pages so the feedback will be unfinished yet.
712        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
713        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
714        $this->assertFalse($result['completed']);
715
716        // Retrieve the unfinished responses.
717        $result = mod_feedback_external::get_unfinished_responses($this->feedback->id);
718        $result = external_api::clean_returnvalue(mod_feedback_external::get_unfinished_responses_returns(), $result);
719        // Check that ids and responses match.
720        foreach ($result['responses'] as $r) {
721            if ($r['item'] == $numericitem->id) {
722                $this->assertEquals(5, $r['value']);
723            } else {
724                $this->assertEquals($textfielditem->id, $r['item']);
725                $this->assertEquals('abc', $r['value']);
726            }
727        }
728    }
729
730    /**
731     * Test get_finished_responses.
732     */
733    public function test_get_finished_responses() {
734        // Test user with full capabilities.
735        $this->setUser($this->student);
736
737        // Create a very simple feedback.
738        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
739        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
740        $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
741
742        $pagedata = [
743            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
744            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
745        ];
746
747        // Process the feedback, there is only one page so the feedback will be completed.
748        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
749        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
750        $this->assertTrue($result['completed']);
751
752        // Retrieve the responses.
753        $result = mod_feedback_external::get_finished_responses($this->feedback->id);
754        $result = external_api::clean_returnvalue(mod_feedback_external::get_finished_responses_returns(), $result);
755        // Check that ids and responses match.
756        foreach ($result['responses'] as $r) {
757            if ($r['item'] == $numericitem->id) {
758                $this->assertEquals(5, $r['value']);
759            } else {
760                $this->assertEquals($textfielditem->id, $r['item']);
761                $this->assertEquals('abc', $r['value']);
762            }
763        }
764    }
765
766    /**
767     * Test get_non_respondents (student trying to get this information).
768     */
769    public function test_get_non_respondents_no_permissions() {
770        $this->setUser($this->student);
771        $this->expectException('moodle_exception');
772        mod_feedback_external::get_non_respondents($this->feedback->id);
773    }
774
775    /**
776     * Test get_non_respondents from an anonymous feedback.
777     */
778    public function test_get_non_respondents_from_anonymous_feedback() {
779        $this->setUser($this->student);
780        $this->expectException('moodle_exception');
781        $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
782        mod_feedback_external::get_non_respondents($this->feedback->id);
783    }
784
785    /**
786     * Test get_non_respondents.
787     */
788    public function test_get_non_respondents() {
789        global $DB;
790
791        // Force non anonymous.
792        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
793
794        // Create another student.
795        $anotherstudent = self::getDataGenerator()->create_user();
796        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
797        $this->setUser($anotherstudent);
798
799        // Test user with full capabilities.
800        $this->setUser($this->student);
801
802        // Create a very simple feedback.
803        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
804        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
805
806        $pagedata = [
807            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
808        ];
809
810        // Process the feedback, there is only one page so the feedback will be completed.
811        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
812        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
813        $this->assertTrue($result['completed']);
814
815        // Retrieve the non-respondent users.
816        $this->setUser($this->teacher);
817        $result = mod_feedback_external::get_non_respondents($this->feedback->id);
818        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
819        $this->assertCount(0, $result['warnings']);
820        $this->assertCount(1, $result['users']);
821        $this->assertEquals($anotherstudent->id, $result['users'][0]['userid']);
822
823        // Create another student.
824        $anotherstudent2 = self::getDataGenerator()->create_user();
825        $this->getDataGenerator()->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual');
826        $this->setUser($anotherstudent2);
827        $this->setUser($this->teacher);
828        $result = mod_feedback_external::get_non_respondents($this->feedback->id);
829        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
830        $this->assertCount(0, $result['warnings']);
831        $this->assertCount(2, $result['users']);
832
833        // Test pagination.
834        $result = mod_feedback_external::get_non_respondents($this->feedback->id, 0, 'lastaccess', 0, 1);
835        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
836        $this->assertCount(0, $result['warnings']);
837        $this->assertCount(1, $result['users']);
838    }
839
840    /**
841     * Helper function that completes the feedback for two students.
842     */
843    protected function complete_basic_feedback() {
844        global $DB;
845
846        $generator = $this->getDataGenerator();
847        // Create separated groups.
848        $DB->set_field('course', 'groupmode', SEPARATEGROUPS);
849        $DB->set_field('course', 'groupmodeforce', 1);
850        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $this->teacherrole->id, $this->context);
851        accesslib_clear_all_caches_for_unit_testing();
852
853        $group1 = $generator->create_group(array('courseid' => $this->course->id));
854        $group2 = $generator->create_group(array('courseid' => $this->course->id));
855
856        // Create another students.
857        $anotherstudent1 = self::getDataGenerator()->create_user();
858        $anotherstudent2 = self::getDataGenerator()->create_user();
859        $generator->enrol_user($anotherstudent1->id, $this->course->id, $this->studentrole->id, 'manual');
860        $generator->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual');
861
862        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->student->id));
863        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->teacher->id));
864        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $anotherstudent1->id));
865        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $anotherstudent2->id));
866
867        // Test user with full capabilities.
868        $this->setUser($this->student);
869
870        // Create a very simple feedback.
871        $feedbackgenerator = $generator->get_plugin_generator('mod_feedback');
872        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
873        $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
874
875        $pagedata = [
876            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
877            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
878        ];
879
880        // Process the feedback, there is only one page so the feedback will be completed.
881        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
882        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
883        $this->assertTrue($result['completed']);
884
885        $this->setUser($anotherstudent1);
886
887        $pagedata = [
888            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10],
889            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'],
890        ];
891
892        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
893        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
894        $this->assertTrue($result['completed']);
895
896        $this->setUser($anotherstudent2);
897
898        $pagedata = [
899            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10],
900            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'],
901        ];
902
903        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
904        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
905        $this->assertTrue($result['completed']);
906    }
907
908    /**
909     * Test get_responses_analysis for anonymous feedback.
910     */
911    public function test_get_responses_analysis_anonymous() {
912        self::complete_basic_feedback();
913
914        // Retrieve the responses analysis.
915        $this->setUser($this->teacher);
916        $result = mod_feedback_external::get_responses_analysis($this->feedback->id);
917        $result = external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result);
918        $this->assertCount(0, $result['warnings']);
919        $this->assertEquals(0, $result['totalattempts']);
920        $this->assertEquals(2, $result['totalanonattempts']);   // Only see my groups.
921
922        foreach ($result['attempts'] as $attempt) {
923            $this->assertEmpty($attempt['userid']); // Is anonymous.
924        }
925    }
926
927    /**
928     * Test get_responses_analysis for non-anonymous feedback.
929     */
930    public function test_get_responses_analysis_non_anonymous() {
931        global $DB;
932
933        // Force non anonymous.
934        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
935
936        self::complete_basic_feedback();
937        // Retrieve the responses analysis.
938        $this->setUser($this->teacher);
939        $result = mod_feedback_external::get_responses_analysis($this->feedback->id);
940        $result = external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result);
941        $this->assertCount(0, $result['warnings']);
942        $this->assertEquals(2, $result['totalattempts']);
943        $this->assertEquals(0, $result['totalanonattempts']);   // Only see my groups.
944
945        foreach ($result['attempts'] as $attempt) {
946            $this->assertNotEmpty($attempt['userid']);  // Is not anonymous.
947        }
948    }
949
950    /**
951     * Test get_last_completed for feedback anonymous not completed.
952     */
953    public function test_get_last_completed_anonymous_not_completed() {
954        global $DB;
955
956        // Force anonymous.
957        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id));
958
959        // Test user with full capabilities that didn't complete the feedback.
960        $this->setUser($this->student);
961
962        $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
963        $this->expectException('moodle_exception');
964        mod_feedback_external::get_last_completed($this->feedback->id);
965    }
966
967    /**
968     * Test get_last_completed for feedback anonymous and completed.
969     */
970    public function test_get_last_completed_anonymous_completed() {
971        global $DB;
972
973        // Force anonymous.
974        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id));
975        // Add one completion record..
976        $record = [
977            'feedback' => $this->feedback->id,
978            'userid' => $this->student->id,
979            'timemodified' => time() - DAYSECS,
980            'random_response' => 0,
981            'anonymous_response' => FEEDBACK_ANONYMOUS_YES,
982            'courseid' => $this->course->id,
983        ];
984        $record['id'] = $DB->insert_record('feedback_completed', (object) $record);
985
986        // Test user with full capabilities.
987        $this->setUser($this->student);
988
989        $this->expectExceptionMessage(get_string('anonymous', 'feedback'));
990        $this->expectException('moodle_exception');
991        mod_feedback_external::get_last_completed($this->feedback->id);
992    }
993
994    /**
995     * Test get_last_completed for feedback not anonymous and completed.
996     */
997    public function test_get_last_completed_not_anonymous_completed() {
998        global $DB;
999
1000        // Force non anonymous.
1001        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1002        // Add one completion record..
1003        $record = [
1004            'feedback' => $this->feedback->id,
1005            'userid' => $this->student->id,
1006            'timemodified' => time() - DAYSECS,
1007            'random_response' => 0,
1008            'anonymous_response' => FEEDBACK_ANONYMOUS_NO,
1009            'courseid' => $this->course->id,
1010        ];
1011        $record['id'] = $DB->insert_record('feedback_completed', (object) $record);
1012
1013        // Test user with full capabilities.
1014        $this->setUser($this->student);
1015        $result = mod_feedback_external::get_last_completed($this->feedback->id);
1016        $result = external_api::clean_returnvalue(mod_feedback_external::get_last_completed_returns(), $result);
1017        $this->assertEquals($record, $result['completed']);
1018    }
1019
1020    /**
1021     * Test get_last_completed for feedback not anonymous and not completed.
1022     */
1023    public function test_get_last_completed_not_anonymous_not_completed() {
1024        global $DB;
1025
1026        // Force anonymous.
1027        $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id));
1028
1029        // Test user with full capabilities that didn't complete the feedback.
1030        $this->setUser($this->student);
1031
1032        $this->expectExceptionMessage(get_string('not_completed_yet', 'feedback'));
1033        $this->expectException('moodle_exception');
1034        mod_feedback_external::get_last_completed($this->feedback->id);
1035    }
1036
1037    /**
1038     * Test get_feedback_access_information for site feedback.
1039     */
1040    public function test_get_feedback_access_information_for_site_feedback() {
1041
1042        $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
1043        $this->setUser($this->student);
1044        // Access the site feedback via the site activity.
1045        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id);
1046        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1047        $this->assertTrue($result['cancomplete']);
1048        $this->assertTrue($result['cansubmit']);
1049
1050        // Access the site feedback via course where I'm enrolled.
1051        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
1052        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1053        $this->assertTrue($result['cancomplete']);
1054        $this->assertTrue($result['cansubmit']);
1055
1056        // Access the site feedback via course where I'm not enrolled.
1057        $othercourse = $this->getDataGenerator()->create_course();
1058
1059        $this->expectException('moodle_exception');
1060        mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
1061    }
1062
1063    /**
1064     * Test get_feedback_access_information for site feedback mapped.
1065     */
1066    public function test_get_feedback_access_information_for_site_feedback_mapped() {
1067        global $DB;
1068
1069        $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
1070        $this->setUser($this->student);
1071        $DB->insert_record('feedback_sitecourse_map', array('feedbackid' => $sitefeedback->id, 'courseid' => $this->course->id));
1072
1073        // Access the site feedback via course where I'm enrolled and mapped.
1074        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
1075        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
1076        $this->assertTrue($result['cancomplete']);
1077        $this->assertTrue($result['cansubmit']);
1078
1079        // Access the site feedback via course where I'm enrolled but not mapped.
1080        $othercourse = $this->getDataGenerator()->create_course();
1081        $this->getDataGenerator()->enrol_user($this->student->id, $othercourse->id, $this->studentrole->id, 'manual');
1082
1083        $this->expectException('moodle_exception');
1084        $this->expectExceptionMessage(get_string('cannotaccess', 'mod_feedback'));
1085        mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
1086    }
1087}
1088