d49167950c0aed0b26eb0544c55042daadbfb91f
[moodle.git] / mod / forum / tests / externallib_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * The module forums external functions unit tests
19  *
20  * @package    mod_forum
21  * @category   external
22  * @copyright  2012 Mark Nelson <markn@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
30 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
31 require_once($CFG->dirroot . '/mod/forum/lib.php');
33 class mod_forum_external_testcase extends externallib_advanced_testcase {
35     /**
36      * Tests set up
37      */
38     protected function setUp() {
39         global $CFG;
41         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
42         // tests using these functions.
43         \mod_forum\subscriptions::reset_forum_cache();
45         require_once($CFG->dirroot . '/mod/forum/externallib.php');
46     }
48     public function tearDown() {
49         // We must clear the subscription caches. This has to be done both before each test, and after in case of other
50         // tests using these functions.
51         \mod_forum\subscriptions::reset_forum_cache();
52     }
54     /**
55      * Test get forums
56      */
57     public function test_mod_forum_get_forums_by_courses() {
58         global $USER, $CFG, $DB;
60         $this->resetAfterTest(true);
62         // Create a user.
63         $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
65         // Set to the user.
66         self::setUser($user);
68         // Create courses to add the modules.
69         $course1 = self::getDataGenerator()->create_course();
70         $course2 = self::getDataGenerator()->create_course();
72         // First forum.
73         $record = new stdClass();
74         $record->introformat = FORMAT_HTML;
75         $record->course = $course1->id;
76         $record->trackingtype = FORUM_TRACKING_FORCED;
77         $forum1 = self::getDataGenerator()->create_module('forum', $record);
79         // Second forum.
80         $record = new stdClass();
81         $record->introformat = FORMAT_HTML;
82         $record->course = $course2->id;
83         $record->trackingtype = FORUM_TRACKING_OFF;
84         $forum2 = self::getDataGenerator()->create_module('forum', $record);
85         $forum2->introfiles = [];
87         // Add discussions to the forums.
88         $record = new stdClass();
89         $record->course = $course1->id;
90         $record->userid = $user->id;
91         $record->forum = $forum1->id;
92         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
93         // Expect one discussion.
94         $forum1->numdiscussions = 1;
95         $forum1->cancreatediscussions = true;
96         $forum1->istracked = true;
97         $forum1->unreadpostscount = 0;
98         $forum1->introfiles = [];
100         $record = new stdClass();
101         $record->course = $course2->id;
102         $record->userid = $user->id;
103         $record->forum = $forum2->id;
104         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
105         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
106         // Expect two discussions.
107         $forum2->numdiscussions = 2;
108         // Default limited role, no create discussion capability enabled.
109         $forum2->cancreatediscussions = false;
110         $forum2->istracked = false;
112         // Check the forum was correctly created.
113         $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
114                 array('forum1' => $forum1->id, 'forum2' => $forum2->id)));
116         // Enrol the user in two courses.
117         // DataGenerator->enrol_user automatically sets a role for the user with the permission mod/form:viewdiscussion.
118         $this->getDataGenerator()->enrol_user($user->id, $course1->id, null, 'manual');
119         // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
120         $enrol = enrol_get_plugin('manual');
121         $enrolinstances = enrol_get_instances($course2->id, true);
122         foreach ($enrolinstances as $courseenrolinstance) {
123             if ($courseenrolinstance->enrol == "manual") {
124                 $instance2 = $courseenrolinstance;
125                 break;
126             }
127         }
128         $enrol->enrol_user($instance2, $user->id);
130         // Assign capabilities to view forums for forum 2.
131         $cm2 = get_coursemodule_from_id('forum', $forum2->cmid, 0, false, MUST_EXIST);
132         $context2 = context_module::instance($cm2->id);
133         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
134         $roleid2 = $this->assignUserCapability('mod/forum:viewdiscussion', $context2->id, $newrole);
136         // Create what we expect to be returned when querying the two courses.
137         unset($forum1->displaywordcount);
138         unset($forum2->displaywordcount);
140         $expectedforums = array();
141         $expectedforums[$forum1->id] = (array) $forum1;
142         $expectedforums[$forum2->id] = (array) $forum2;
144         // Call the external function passing course ids.
145         $forums = mod_forum_external::get_forums_by_courses(array($course1->id, $course2->id));
146         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
147         $this->assertCount(2, $forums);
148         foreach ($forums as $forum) {
149             $this->assertEquals($expectedforums[$forum['id']], $forum);
150         }
152         // Call the external function without passing course id.
153         $forums = mod_forum_external::get_forums_by_courses();
154         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
155         $this->assertCount(2, $forums);
156         foreach ($forums as $forum) {
157             $this->assertEquals($expectedforums[$forum['id']], $forum);
158         }
160         // Unenrol user from second course and alter expected forums.
161         $enrol->unenrol_user($instance2, $user->id);
162         unset($expectedforums[$forum2->id]);
164         // Call the external function without passing course id.
165         $forums = mod_forum_external::get_forums_by_courses();
166         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
167         $this->assertCount(1, $forums);
168         $this->assertEquals($expectedforums[$forum1->id], $forums[0]);
169         $this->assertTrue($forums[0]['cancreatediscussions']);
171         // Change the type of the forum, the user shouldn't be able to add discussions.
172         $DB->set_field('forum', 'type', 'news', array('id' => $forum1->id));
173         $forums = mod_forum_external::get_forums_by_courses();
174         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
175         $this->assertFalse($forums[0]['cancreatediscussions']);
177         // Call for the second course we unenrolled the user from.
178         $forums = mod_forum_external::get_forums_by_courses(array($course2->id));
179         $forums = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $forums);
180         $this->assertCount(0, $forums);
181     }
183     /**
184      * Test the toggle favourite state
185      */
186     public function test_mod_forum_toggle_favourite_state() {
187         global $USER, $CFG, $DB;
189         $this->resetAfterTest(true);
191         // Create a user.
192         $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
194         // Set to the user.
195         self::setUser($user);
197         // Create courses to add the modules.
198         $course1 = self::getDataGenerator()->create_course();
199         $this->getDataGenerator()->enrol_user($user->id, $course1->id);
201         $record = new stdClass();
202         $record->introformat = FORMAT_HTML;
203         $record->course = $course1->id;
204         $record->trackingtype = FORUM_TRACKING_OFF;
205         $forum1 = self::getDataGenerator()->create_module('forum', $record);
206         $forum1->introfiles = [];
208         // Add discussions to the forums.
209         $record = new stdClass();
210         $record->course = $course1->id;
211         $record->userid = $user->id;
212         $record->forum = $forum1->id;
213         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
215         $response = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
216         $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
217         $this->assertTrue($response['userstate']['favourited']);
219         $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
220         $response = external_api::clean_returnvalue(mod_forum_external::toggle_favourite_state_returns(), $response);
221         $this->assertFalse($response['userstate']['favourited']);
223         $this->setUser(0);
224         try {
225             $response = mod_forum_external::toggle_favourite_state($discussion1->id, 0);
226         } catch (moodle_exception $e) {
227             $this->assertEquals('requireloginerror', $e->errorcode);
228         }
229     }
231     /**
232      * Test the toggle pin state
233      */
234     public function test_mod_forum_set_pin_state() {
235         $this->resetAfterTest(true);
237         // Create a user.
238         $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
240         // Set to the user.
241         self::setUser($user);
243         // Create courses to add the modules.
244         $course1 = self::getDataGenerator()->create_course();
245         $this->getDataGenerator()->enrol_user($user->id, $course1->id);
247         $record = new stdClass();
248         $record->introformat = FORMAT_HTML;
249         $record->course = $course1->id;
250         $record->trackingtype = FORUM_TRACKING_OFF;
251         $forum1 = self::getDataGenerator()->create_module('forum', $record);
252         $forum1->introfiles = [];
254         // Add discussions to the forums.
255         $record = new stdClass();
256         $record->course = $course1->id;
257         $record->userid = $user->id;
258         $record->forum = $forum1->id;
259         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
261         try {
262             $response = mod_forum_external::set_pin_state($discussion1->id, 1);
263         } catch (Exception $e) {
264             $this->assertEquals('cannotpindiscussions', $e->errorcode);
265         }
267         self::setAdminUser();
268         $response = mod_forum_external::set_pin_state($discussion1->id, 1);
269         $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
270         $this->assertTrue($response['pinned']);
272         $response = mod_forum_external::set_pin_state($discussion1->id, 0);
273         $response = external_api::clean_returnvalue(mod_forum_external::set_pin_state_returns(), $response);
274         $this->assertFalse($response['pinned']);
275     }
277     /**
278      * Test get forum posts
279      */
280     public function test_mod_forum_get_forum_discussion_posts() {
281         global $CFG, $PAGE;
283         $this->resetAfterTest(true);
285         // Set the CFG variable to allow track forums.
286         $CFG->forum_trackreadposts = true;
288         // Create a user who can track forums.
289         $record = new stdClass();
290         $record->trackforums = true;
291         $user1 = self::getDataGenerator()->create_user($record);
292         // Create a bunch of other users to post.
293         $user2 = self::getDataGenerator()->create_user();
294         $user3 = self::getDataGenerator()->create_user();
296         // Set the first created user to the test user.
297         self::setUser($user1);
299         // Create course to add the module.
300         $course1 = self::getDataGenerator()->create_course();
302         // Forum with tracking off.
303         $record = new stdClass();
304         $record->course = $course1->id;
305         $record->trackingtype = FORUM_TRACKING_OFF;
306         $forum1 = self::getDataGenerator()->create_module('forum', $record);
307         $forum1context = context_module::instance($forum1->cmid);
309         // Forum with tracking enabled.
310         $record = new stdClass();
311         $record->course = $course1->id;
312         $forum2 = self::getDataGenerator()->create_module('forum', $record);
313         $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
314         $forum2context = context_module::instance($forum2->cmid);
316         // Add discussions to the forums.
317         $record = new stdClass();
318         $record->course = $course1->id;
319         $record->userid = $user1->id;
320         $record->forum = $forum1->id;
321         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
323         $record = new stdClass();
324         $record->course = $course1->id;
325         $record->userid = $user2->id;
326         $record->forum = $forum1->id;
327         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
329         $record = new stdClass();
330         $record->course = $course1->id;
331         $record->userid = $user2->id;
332         $record->forum = $forum2->id;
333         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
335         // Add 2 replies to the discussion 1 from different users.
336         $record = new stdClass();
337         $record->discussion = $discussion1->id;
338         $record->parent = $discussion1->firstpost;
339         $record->userid = $user2->id;
340         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
341         $filename = 'shouldbeanimage.jpg';
342         // Add a fake inline image to the post.
343         $filerecordinline = array(
344             'contextid' => $forum1context->id,
345             'component' => 'mod_forum',
346             'filearea'  => 'post',
347             'itemid'    => $discussion1reply1->id,
348             'filepath'  => '/',
349             'filename'  => $filename,
350         );
351         $fs = get_file_storage();
352         $timepost = time();
353         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
355         $record->parent = $discussion1reply1->id;
356         $record->userid = $user3->id;
357         $record->tags = array('Cats', 'Dogs');
358         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
360         // Enrol the user in the  course.
361         $enrol = enrol_get_plugin('manual');
362         // Following line enrol and assign default role id to the user.
363         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
364         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
365         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
367         // Delete one user, to test that we still receive posts by this user.
368         delete_user($user3);
370         // Create what we expect to be returned when querying the discussion.
371         $expectedposts = array(
372             'posts' => array(),
373             'ratinginfo' => array(
374                 'contextid' => $forum1context->id,
375                 'component' => 'mod_forum',
376                 'ratingarea' => 'post',
377                 'canviewall' => null,
378                 'canviewany' => null,
379                 'scales' => array(),
380                 'ratings' => array(),
381             ),
382             'warnings' => array(),
383         );
385         // User pictures are initially empty, we should get the links once the external function is called.
386         $expectedposts['posts'][] = array(
387             'id' => $discussion1reply2->id,
388             'discussion' => $discussion1reply2->discussion,
389             'parent' => $discussion1reply2->parent,
390             'userid' => (int) $discussion1reply2->userid,
391             'created' => $discussion1reply2->created,
392             'modified' => $discussion1reply2->modified,
393             'mailed' => $discussion1reply2->mailed,
394             'subject' => $discussion1reply2->subject,
395             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
396                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
397             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
398             'messagetrust' => $discussion1reply2->messagetrust,
399             'attachment' => $discussion1reply2->attachment,
400             'totalscore' => $discussion1reply2->totalscore,
401             'mailnow' => $discussion1reply2->mailnow,
402             'children' => array(),
403             'canreply' => true,
404             'postread' => false,
405             'userfullname' => fullname($user3),
406             'userpictureurl' => '',
407             'deleted' => false,
408             'isprivatereply' => false,
409             'tags' => \core_tag\external\util::get_item_tags('mod_forum', 'forum_posts', $discussion1reply2->id),
410         );
411         // Cast to expected.
412         $this->assertCount(2, $expectedposts['posts'][0]['tags']);
413         $expectedposts['posts'][0]['tags'][0]['isstandard'] = (bool) $expectedposts['posts'][0]['tags'][0]['isstandard'];
414         $expectedposts['posts'][0]['tags'][1]['isstandard'] = (bool) $expectedposts['posts'][0]['tags'][1]['isstandard'];
416         $expectedposts['posts'][] = array(
417             'id' => $discussion1reply1->id,
418             'discussion' => $discussion1reply1->discussion,
419             'parent' => $discussion1reply1->parent,
420             'userid' => (int) $discussion1reply1->userid,
421             'created' => $discussion1reply1->created,
422             'modified' => $discussion1reply1->modified,
423             'mailed' => $discussion1reply1->mailed,
424             'subject' => $discussion1reply1->subject,
425             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
426                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
427             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
428             'messagetrust' => $discussion1reply1->messagetrust,
429             'attachment' => $discussion1reply1->attachment,
430             'messageinlinefiles' => array(
431                 array(
432                     'filename' => $filename,
433                     'filepath' => '/',
434                     'filesize' => '27',
435                     'fileurl' => moodle_url::make_webservice_pluginfile_url($forum1context->id, 'mod_forum', 'post',
436                                     $discussion1reply1->id, '/', $filename),
437                     'timemodified' => $timepost,
438                     'mimetype' => 'image/jpeg',
439                     'isexternalfile' => false,
440                 )
441             ),
442             'totalscore' => $discussion1reply1->totalscore,
443             'mailnow' => $discussion1reply1->mailnow,
444             'children' => array($discussion1reply2->id),
445             'canreply' => true,
446             'postread' => false,
447             'userfullname' => fullname($user2),
448             'userpictureurl' => '',
449             'deleted' => false,
450             'isprivatereply' => false,
451             'tags' => array(),
452         );
454         // Test a discussion with two additional posts (total 3 posts).
455         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
456         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
457         $this->assertEquals(3, count($posts['posts']));
459         // Generate here the pictures because we need to wait to the external function to init the theme.
460         $userpicture = new user_picture($user3);
461         $userpicture->size = 1; // Size f1.
462         $expectedposts['posts'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
464         $userpicture = new user_picture($user2);
465         $userpicture->size = 1; // Size f1.
466         $expectedposts['posts'][1]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
468         // Unset the initial discussion post.
469         array_pop($posts['posts']);
470         $this->assertEquals($expectedposts, $posts);
472         // Check we receive the unread count correctly on tracked forum.
473         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
474         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
475         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
476         foreach ($result as $f) {
477             if ($f['id'] == $forum2->id) {
478                 $this->assertEquals(1, $f['unreadpostscount']);
479             }
480         }
482         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
483         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id, 'modified', 'DESC');
484         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
485         $this->assertEquals(1, count($posts['posts']));
487         // Test discussion tracking on not tracked forum.
488         $result = mod_forum_external::view_forum_discussion($discussion1->id);
489         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
490         $this->assertTrue($result['status']);
491         $this->assertEmpty($result['warnings']);
493         // Test posts have not been marked as read.
494         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
495         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
496         foreach ($posts['posts'] as $post) {
497             $this->assertFalse($post['postread']);
498         }
500         // Test discussion tracking on tracked forum.
501         $result = mod_forum_external::view_forum_discussion($discussion3->id);
502         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
503         $this->assertTrue($result['status']);
504         $this->assertEmpty($result['warnings']);
506         // Test posts have been marked as read.
507         $posts = mod_forum_external::get_forum_discussion_posts($discussion3->id, 'modified', 'DESC');
508         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
509         foreach ($posts['posts'] as $post) {
510             $this->assertTrue($post['postread']);
511         }
513         // Check we receive 0 unread posts.
514         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
515         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
516         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
517         foreach ($result as $f) {
518             if ($f['id'] == $forum2->id) {
519                 $this->assertEquals(0, $f['unreadpostscount']);
520             }
521         }
522     }
524     /**
525      * Test get forum posts
526      *
527      * Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities
528      */
529     public function test_mod_forum_get_discussion_posts() {
530         global $CFG, $PAGE;
532         $this->resetAfterTest(true);
534         // Set the CFG variable to allow track forums.
535         $CFG->forum_trackreadposts = true;
537         $urlfactory = mod_forum\local\container::get_url_factory();
538         $legacyfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
539         $entityfactory = mod_forum\local\container::get_entity_factory();
541         // Create a user who can track forums.
542         $record = new stdClass();
543         $record->trackforums = true;
544         $user1 = self::getDataGenerator()->create_user($record);
545         // Create a bunch of other users to post.
546         $user2 = self::getDataGenerator()->create_user();
547         $user2entity = $entityfactory->get_author_from_stdclass($user2);
548         $exporteduser2 = [
549             'id' => (int) $user2->id,
550             'fullname' => fullname($user2),
551             'isdeleted' => false,
552             'groups' => [],
553             'urls' => [
554                 'profile' => $urlfactory->get_author_profile_url($user2entity),
555                 'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
556             ]
557         ];
558         $user2->fullname = $exporteduser2['fullname'];
560         $user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]);
561         $user3entity = $entityfactory->get_author_from_stdclass($user3);
562         $exporteduser3 = [
563             'id' => (int) $user3->id,
564             'fullname' => fullname($user3),
565             'groups' => [],
566             'isdeleted' => false,
567             'urls' => [
568                 'profile' => $urlfactory->get_author_profile_url($user3entity),
569                 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
570             ]
571         ];
572         $user3->fullname = $exporteduser3['fullname'];
573         $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
575         // Set the first created user to the test user.
576         self::setUser($user1);
578         // Create course to add the module.
579         $course1 = self::getDataGenerator()->create_course();
581         // Forum with tracking off.
582         $record = new stdClass();
583         $record->course = $course1->id;
584         $record->trackingtype = FORUM_TRACKING_OFF;
585         // Display word count. Otherwise, word and char counts will be set to null by the forum post exporter.
586         $record->displaywordcount = true;
587         $forum1 = self::getDataGenerator()->create_module('forum', $record);
588         $forum1context = context_module::instance($forum1->cmid);
590         // Forum with tracking enabled.
591         $record = new stdClass();
592         $record->course = $course1->id;
593         $forum2 = self::getDataGenerator()->create_module('forum', $record);
594         $forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
595         $forum2context = context_module::instance($forum2->cmid);
597         // Add discussions to the forums.
598         $record = new stdClass();
599         $record->course = $course1->id;
600         $record->userid = $user1->id;
601         $record->forum = $forum1->id;
602         $discussion1 = $forumgenerator->create_discussion($record);
604         $record = new stdClass();
605         $record->course = $course1->id;
606         $record->userid = $user2->id;
607         $record->forum = $forum1->id;
608         $discussion2 = $forumgenerator->create_discussion($record);
610         $record = new stdClass();
611         $record->course = $course1->id;
612         $record->userid = $user2->id;
613         $record->forum = $forum2->id;
614         $discussion3 = $forumgenerator->create_discussion($record);
616         // Add 2 replies to the discussion 1 from different users.
617         $record = new stdClass();
618         $record->discussion = $discussion1->id;
619         $record->parent = $discussion1->firstpost;
620         $record->userid = $user2->id;
621         $discussion1reply1 = $forumgenerator->create_post($record);
622         $filename = 'shouldbeanimage.jpg';
623         // Add a fake inline image to the post.
624         $filerecordinline = array(
625             'contextid' => $forum1context->id,
626             'component' => 'mod_forum',
627             'filearea'  => 'post',
628             'itemid'    => $discussion1reply1->id,
629             'filepath'  => '/',
630             'filename'  => $filename,
631         );
632         $fs = get_file_storage();
633         $timepost = time();
634         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
636         $record->parent = $discussion1reply1->id;
637         $record->userid = $user3->id;
638         $discussion1reply2 = $forumgenerator->create_post($record);
640         // Enrol the user in the  course.
641         $enrol = enrol_get_plugin('manual');
642         // Following line enrol and assign default role id to the user.
643         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
644         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
645         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
647         // Delete one user, to test that we still receive posts by this user.
648         delete_user($user3);
649         $exporteduser3 = [
650             'id' => (int) $user3->id,
651             'fullname' => get_string('deleteduser', 'mod_forum'),
652             'groups' => [],
653             'isdeleted' => true,
654             'urls' => [
655                 'profile' => $urlfactory->get_author_profile_url($user3entity),
656                 'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
657             ]
658         ];
660         // Create what we expect to be returned when querying the discussion.
661         $expectedposts = array(
662             'posts' => array(),
663             'ratinginfo' => array(
664                 'contextid' => $forum1context->id,
665                 'component' => 'mod_forum',
666                 'ratingarea' => 'post',
667                 'canviewall' => null,
668                 'canviewany' => null,
669                 'scales' => array(),
670                 'ratings' => array(),
671             ),
672             'warnings' => array(),
673         );
675         // User pictures are initially empty, we should get the links once the external function is called.
676         $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion);
677         $isolatedurl->params(['parent' => $discussion1reply2->id]);
678         $message = file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
679             $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id);
680         $expectedposts['posts'][] = array(
681             'id' => $discussion1reply2->id,
682             'discussionid' => $discussion1reply2->discussion,
683             'parentid' => $discussion1reply2->parent,
684             'hasparent' => true,
685             'timecreated' => $discussion1reply2->created,
686             'subject' => $discussion1reply2->subject,
687             'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
688             'message' => $message,
689             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
690             'unread' => null,
691             'isdeleted' => false,
692             'isprivatereply' => false,
693             'haswordcount' => true,
694             'wordcount' => count_words($message),
695             'charcount' => count_letters($message),
696             'author'=> $exporteduser3,
697             'attachments' => [],
698             'tags' => [],
699             'html' => [
700                 'rating' => null,
701                 'taglist' => null,
702                 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created)
703             ],
704             'capabilities' => [
705                 'view' => 1,
706                 'edit' => 0,
707                 'delete' => 0,
708                 'split' => 0,
709                 'reply' => 1,
710                 'export' => 0,
711                 'controlreadstatus' => 0,
712                 'canreplyprivately' => 0,
713                 'selfenrol' => 0
714             ],
715             'urls' => [
716                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
717                 'viewisolated' => $isolatedurl->out(false),
718                 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent),
719                 'edit' => null,
720                 'delete' =>null,
721                 'split' => null,
722                 'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
723                     'reply' => $discussion1reply2->id
724                 ]))->out(false),
725                 'export' => null,
726                 'markasread' => null,
727                 'markasunread' => null,
728                 'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion),
729             ],
730         );
733         $isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
734         $isolatedurl->params(['parent' => $discussion1reply1->id]);
735         $message = file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
736             $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id);
737         $expectedposts['posts'][] = array(
738             'id' => $discussion1reply1->id,
739             'discussionid' => $discussion1reply1->discussion,
740             'parentid' => $discussion1reply1->parent,
741             'hasparent' => true,
742             'timecreated' => $discussion1reply1->created,
743             'subject' => $discussion1reply1->subject,
744             'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
745             'message' => $message,
746             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
747             'unread' => null,
748             'isdeleted' => false,
749             'isprivatereply' => false,
750             'haswordcount' => true,
751             'wordcount' => count_words($message),
752             'charcount' => count_letters($message),
753             'author'=> $exporteduser2,
754             'attachments' => [],
755             'tags' => [],
756             'html' => [
757                 'rating' => null,
758                 'taglist' => null,
759                 'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created)
760             ],
761             'capabilities' => [
762                 'view' => 1,
763                 'edit' => 0,
764                 'delete' => 0,
765                 'split' => 0,
766                 'reply' => 1,
767                 'export' => 0,
768                 'controlreadstatus' => 0,
769                 'canreplyprivately' => 0,
770                 'selfenrol' => 0
771             ],
772             'urls' => [
773                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
774                 'viewisolated' => $isolatedurl->out(false),
775                 'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent),
776                 'edit' => null,
777                 'delete' =>null,
778                 'split' => null,
779                 'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
780                     'reply' => $discussion1reply1->id
781                 ]))->out(false),
782                 'export' => null,
783                 'markasread' => null,
784                 'markasunread' => null,
785                 'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion),
786             ],
787         );
789         // Test a discussion with two additional posts (total 3 posts).
790         $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
791         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
792         $this->assertEquals(3, count($posts['posts']));
794         // Unset the initial discussion post.
795         array_pop($posts['posts']);
796         $this->assertEquals($expectedposts, $posts);
798         // Check we receive the unread count correctly on tracked forum.
799         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
800         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
801         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
802         foreach ($result as $f) {
803             if ($f['id'] == $forum2->id) {
804                 $this->assertEquals(1, $f['unreadpostscount']);
805             }
806         }
808         // Test discussion without additional posts. There should be only one post (the one created by the discussion).
809         $posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
810         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
811         $this->assertEquals(1, count($posts['posts']));
813         // Test discussion tracking on not tracked forum.
814         $result = mod_forum_external::view_forum_discussion($discussion1->id);
815         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
816         $this->assertTrue($result['status']);
817         $this->assertEmpty($result['warnings']);
819         // Test posts have not been marked as read.
820         $posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
821         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
822         foreach ($posts['posts'] as $post) {
823             $this->assertNull($post['unread']);
824         }
826         // Test discussion tracking on tracked forum.
827         $result = mod_forum_external::view_forum_discussion($discussion3->id);
828         $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
829         $this->assertTrue($result['status']);
830         $this->assertEmpty($result['warnings']);
832         // Test posts have been marked as read.
833         $posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC');
834         $posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
835         foreach ($posts['posts'] as $post) {
836             $this->assertFalse($post['unread']);
837         }
839         // Check we receive 0 unread posts.
840         forum_tp_count_forum_unread_posts($forum2cm, $course1, true);    // Reset static cache.
841         $result = mod_forum_external::get_forums_by_courses(array($course1->id));
842         $result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
843         foreach ($result as $f) {
844             if ($f['id'] == $forum2->id) {
845                 $this->assertEquals(0, $f['unreadpostscount']);
846             }
847         }
848     }
850     /**
851      * Test get forum posts
852      */
853     public function test_mod_forum_get_forum_discussion_posts_deleted() {
854         global $CFG, $PAGE;
856         $this->resetAfterTest(true);
857         $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
859         // Create a course and enrol some users in it.
860         $course1 = self::getDataGenerator()->create_course();
862         // Create users.
863         $user1 = self::getDataGenerator()->create_user();
864         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
865         $user2 = self::getDataGenerator()->create_user();
866         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
868         // Set the first created user to the test user.
869         self::setUser($user1);
871         // Create test data.
872         $forum1 = self::getDataGenerator()->create_module('forum', (object) [
873                 'course' => $course1->id,
874             ]);
875         $forum1context = context_module::instance($forum1->cmid);
877         // Add discussions to the forum.
878         $discussion = $generator->create_discussion((object) [
879                 'course' => $course1->id,
880                 'userid' => $user1->id,
881                 'forum' => $forum1->id,
882             ]);
884         $discussion2 = $generator->create_discussion((object) [
885                 'course' => $course1->id,
886                 'userid' => $user2->id,
887                 'forum' => $forum1->id,
888             ]);
890         // Add replies to the discussion.
891         $discussionreply1 = $generator->create_post((object) [
892                 'discussion' => $discussion->id,
893                 'parent' => $discussion->firstpost,
894                 'userid' => $user2->id,
895             ]);
896         $discussionreply2 = $generator->create_post((object) [
897                 'discussion' => $discussion->id,
898                 'parent' => $discussionreply1->id,
899                 'userid' => $user2->id,
900                 'subject' => '',
901                 'message' => '',
902                 'messageformat' => FORMAT_PLAIN,
903                 'deleted' => 1,
904             ]);
905         $discussionreply3 = $generator->create_post((object) [
906                 'discussion' => $discussion->id,
907                 'parent' => $discussion->firstpost,
908                 'userid' => $user2->id,
909             ]);
911         // Test where some posts have been marked as deleted.
912         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id, 'modified', 'DESC');
913         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
914         $deletedsubject = get_string('privacy:request:delete:post:subject', 'mod_forum');
915         $deletedmessage = get_string('privacy:request:delete:post:message', 'mod_forum');
917         foreach ($posts['posts'] as $post) {
918             if ($post['id'] == $discussionreply2->id) {
919                 $this->assertTrue($post['deleted']);
920                 $this->assertEquals($deletedsubject, $post['subject']);
921                 $this->assertEquals($deletedmessage, $post['message']);
922             } else {
923                 $this->assertFalse($post['deleted']);
924                 $this->assertNotEquals($deletedsubject, $post['subject']);
925                 $this->assertNotEquals($deletedmessage, $post['message']);
926             }
927         }
928     }
930     /**
931      * Test get forum posts (qanda forum)
932      */
933     public function test_mod_forum_get_forum_discussion_posts_qanda() {
934         global $CFG, $DB;
936         $this->resetAfterTest(true);
938         $record = new stdClass();
939         $user1 = self::getDataGenerator()->create_user($record);
940         $user2 = self::getDataGenerator()->create_user();
942         // Set the first created user to the test user.
943         self::setUser($user1);
945         // Create course to add the module.
946         $course1 = self::getDataGenerator()->create_course();
947         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
948         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
950         // Forum with tracking off.
951         $record = new stdClass();
952         $record->course = $course1->id;
953         $record->type = 'qanda';
954         $forum1 = self::getDataGenerator()->create_module('forum', $record);
955         $forum1context = context_module::instance($forum1->cmid);
957         // Add discussions to the forums.
958         $record = new stdClass();
959         $record->course = $course1->id;
960         $record->userid = $user2->id;
961         $record->forum = $forum1->id;
962         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
964         // Add 1 reply (not the actual user).
965         $record = new stdClass();
966         $record->discussion = $discussion1->id;
967         $record->parent = $discussion1->firstpost;
968         $record->userid = $user2->id;
969         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
971         // We still see only the original post.
972         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
973         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
974         $this->assertEquals(1, count($posts['posts']));
976         // Add a new reply, the user is going to be able to see only the original post and their new post.
977         $record = new stdClass();
978         $record->discussion = $discussion1->id;
979         $record->parent = $discussion1->firstpost;
980         $record->userid = $user1->id;
981         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
983         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
984         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
985         $this->assertEquals(2, count($posts['posts']));
987         // Now, we can fake the time of the user post, so he can se the rest of the discussion posts.
988         $discussion1reply2->created -= $CFG->maxeditingtime * 2;
989         $DB->update_record('forum_posts', $discussion1reply2);
991         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
992         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
993         $this->assertEquals(3, count($posts['posts']));
994     }
996     /**
997      * Test get forum discussions paginated
998      */
999     public function test_mod_forum_get_forum_discussions_paginated() {
1000         global $USER, $CFG, $DB, $PAGE;
1002         $this->resetAfterTest(true);
1004         // Set the CFG variable to allow track forums.
1005         $CFG->forum_trackreadposts = true;
1007         // Create a user who can track forums.
1008         $record = new stdClass();
1009         $record->trackforums = true;
1010         $user1 = self::getDataGenerator()->create_user($record);
1011         // Create a bunch of other users to post.
1012         $user2 = self::getDataGenerator()->create_user();
1013         $user3 = self::getDataGenerator()->create_user();
1014         $user4 = self::getDataGenerator()->create_user();
1016         // Set the first created user to the test user.
1017         self::setUser($user1);
1019         // Create courses to add the modules.
1020         $course1 = self::getDataGenerator()->create_course();
1022         // First forum with tracking off.
1023         $record = new stdClass();
1024         $record->course = $course1->id;
1025         $record->trackingtype = FORUM_TRACKING_OFF;
1026         $forum1 = self::getDataGenerator()->create_module('forum', $record);
1028         // Add discussions to the forums.
1029         $record = new stdClass();
1030         $record->course = $course1->id;
1031         $record->userid = $user1->id;
1032         $record->forum = $forum1->id;
1033         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1035         // Add three replies to the discussion 1 from different users.
1036         $record = new stdClass();
1037         $record->discussion = $discussion1->id;
1038         $record->parent = $discussion1->firstpost;
1039         $record->userid = $user2->id;
1040         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1042         $record->parent = $discussion1reply1->id;
1043         $record->userid = $user3->id;
1044         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1046         $record->userid = $user4->id;
1047         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1049         // Enrol the user in the first course.
1050         $enrol = enrol_get_plugin('manual');
1052         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1053         $enrolinstances = enrol_get_instances($course1->id, true);
1054         foreach ($enrolinstances as $courseenrolinstance) {
1055             if ($courseenrolinstance->enrol == "manual") {
1056                 $instance1 = $courseenrolinstance;
1057                 break;
1058             }
1059         }
1060         $enrol->enrol_user($instance1, $user1->id);
1062         // Delete one user.
1063         delete_user($user4);
1065         // Assign capabilities to view discussions for forum 1.
1066         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1067         $context = context_module::instance($cm->id);
1068         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1069         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1071         // Create what we expect to be returned when querying the forums.
1073         $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
1075         // User pictures are initially empty, we should get the links once the external function is called.
1076         $expecteddiscussions = array(
1077                 'id' => $discussion1->firstpost,
1078                 'name' => $discussion1->name,
1079                 'groupid' => (int) $discussion1->groupid,
1080                 'timemodified' => $discussion1reply3->created,
1081                 'usermodified' => (int) $discussion1reply3->userid,
1082                 'timestart' => (int) $discussion1->timestart,
1083                 'timeend' => (int) $discussion1->timeend,
1084                 'discussion' => $discussion1->id,
1085                 'parent' => 0,
1086                 'userid' => (int) $discussion1->userid,
1087                 'created' => (int) $post1->created,
1088                 'modified' => (int) $post1->modified,
1089                 'mailed' => (int) $post1->mailed,
1090                 'subject' => $post1->subject,
1091                 'message' => $post1->message,
1092                 'messageformat' => (int) $post1->messageformat,
1093                 'messagetrust' => (int) $post1->messagetrust,
1094                 'attachment' => $post1->attachment,
1095                 'totalscore' => (int) $post1->totalscore,
1096                 'mailnow' => (int) $post1->mailnow,
1097                 'userfullname' => fullname($user1),
1098                 'usermodifiedfullname' => fullname($user4),
1099                 'userpictureurl' => '',
1100                 'usermodifiedpictureurl' => '',
1101                 'numreplies' => 3,
1102                 'numunread' => 0,
1103                 'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
1104                 'locked' => false,
1105                 'canreply' => false,
1106                 'canlock' => false
1107             );
1109         // Call the external function passing forum id.
1110         $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
1111         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1112         $expectedreturn = array(
1113             'discussions' => array($expecteddiscussions),
1114             'warnings' => array()
1115         );
1117         // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1118         $userpicture = new user_picture($user1);
1119         $userpicture->size = 1; // Size f1.
1120         $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1122         $userpicture = new user_picture($user4);
1123         $userpicture->size = 1; // Size f1.
1124         $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1126         $this->assertEquals($expectedreturn, $discussions);
1128         // Call without required view discussion capability.
1129         $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1130         try {
1131             mod_forum_external::get_forum_discussions_paginated($forum1->id);
1132             $this->fail('Exception expected due to missing capability.');
1133         } catch (moodle_exception $e) {
1134             $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1135         }
1137         // Unenrol user from second course.
1138         $enrol->unenrol_user($instance1, $user1->id);
1140         // Call for the second course we unenrolled the user from, make sure exception thrown.
1141         try {
1142             mod_forum_external::get_forum_discussions_paginated($forum1->id);
1143             $this->fail('Exception expected due to being unenrolled from the course.');
1144         } catch (moodle_exception $e) {
1145             $this->assertEquals('requireloginerror', $e->errorcode);
1146         }
1148         $this->setAdminUser();
1149         $discussions = mod_forum_external::get_forum_discussions_paginated($forum1->id);
1150         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1151         $this->assertTrue($discussions['discussions'][0]['canlock']);
1152     }
1154     /**
1155      * Test get forum discussions paginated (qanda forums)
1156      */
1157     public function test_mod_forum_get_forum_discussions_paginated_qanda() {
1159         $this->resetAfterTest(true);
1161         // Create courses to add the modules.
1162         $course = self::getDataGenerator()->create_course();
1164         $user1 = self::getDataGenerator()->create_user();
1165         $user2 = self::getDataGenerator()->create_user();
1167         // First forum with tracking off.
1168         $record = new stdClass();
1169         $record->course = $course->id;
1170         $record->type = 'qanda';
1171         $forum = self::getDataGenerator()->create_module('forum', $record);
1173         // Add discussions to the forums.
1174         $discussionrecord = new stdClass();
1175         $discussionrecord->course = $course->id;
1176         $discussionrecord->userid = $user2->id;
1177         $discussionrecord->forum = $forum->id;
1178         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
1180         self::setAdminUser();
1181         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1182         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1184         $this->assertCount(1, $discussions['discussions']);
1185         $this->assertCount(0, $discussions['warnings']);
1187         self::setUser($user1);
1188         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1190         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1191         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1193         $this->assertCount(1, $discussions['discussions']);
1194         $this->assertCount(0, $discussions['warnings']);
1196     }
1198     /**
1199      * Test get forum discussions
1200      */
1201     public function test_mod_forum_get_forum_discussions() {
1202         global $CFG, $DB, $PAGE;
1204         $this->resetAfterTest(true);
1206         // Set the CFG variable to allow track forums.
1207         $CFG->forum_trackreadposts = true;
1209         // Create a user who can track forums.
1210         $record = new stdClass();
1211         $record->trackforums = true;
1212         $user1 = self::getDataGenerator()->create_user($record);
1213         // Create a bunch of other users to post.
1214         $user2 = self::getDataGenerator()->create_user();
1215         $user3 = self::getDataGenerator()->create_user();
1216         $user4 = self::getDataGenerator()->create_user();
1218         // Set the first created user to the test user.
1219         self::setUser($user1);
1221         // Create courses to add the modules.
1222         $course1 = self::getDataGenerator()->create_course();
1224         // First forum with tracking off.
1225         $record = new stdClass();
1226         $record->course = $course1->id;
1227         $record->trackingtype = FORUM_TRACKING_OFF;
1228         $forum1 = self::getDataGenerator()->create_module('forum', $record);
1230         // Add discussions to the forums.
1231         $record = new stdClass();
1232         $record->course = $course1->id;
1233         $record->userid = $user1->id;
1234         $record->forum = $forum1->id;
1235         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1237         // Add three replies to the discussion 1 from different users.
1238         $record = new stdClass();
1239         $record->discussion = $discussion1->id;
1240         $record->parent = $discussion1->firstpost;
1241         $record->userid = $user2->id;
1242         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1244         $record->parent = $discussion1reply1->id;
1245         $record->userid = $user3->id;
1246         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1248         $record->userid = $user4->id;
1249         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1251         // Enrol the user in the first course.
1252         $enrol = enrol_get_plugin('manual');
1254         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1255         $enrolinstances = enrol_get_instances($course1->id, true);
1256         foreach ($enrolinstances as $courseenrolinstance) {
1257             if ($courseenrolinstance->enrol == "manual") {
1258                 $instance1 = $courseenrolinstance;
1259                 break;
1260             }
1261         }
1262         $enrol->enrol_user($instance1, $user1->id);
1264         // Delete one user.
1265         delete_user($user4);
1267         // Assign capabilities to view discussions for forum 1.
1268         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1269         $context = context_module::instance($cm->id);
1270         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1271         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1273         // Create what we expect to be returned when querying the forums.
1275         $post1 = $DB->get_record('forum_posts', array('id' => $discussion1->firstpost), '*', MUST_EXIST);
1277         // User pictures are initially empty, we should get the links once the external function is called.
1278         $expecteddiscussions = array(
1279             'id' => $discussion1->firstpost,
1280             'name' => $discussion1->name,
1281             'groupid' => (int) $discussion1->groupid,
1282             'timemodified' => (int) $discussion1reply3->created,
1283             'usermodified' => (int) $discussion1reply3->userid,
1284             'timestart' => (int) $discussion1->timestart,
1285             'timeend' => (int) $discussion1->timeend,
1286             'discussion' => (int) $discussion1->id,
1287             'parent' => 0,
1288             'userid' => (int) $discussion1->userid,
1289             'created' => (int) $post1->created,
1290             'modified' => (int) $post1->modified,
1291             'mailed' => (int) $post1->mailed,
1292             'subject' => $post1->subject,
1293             'message' => $post1->message,
1294             'messageformat' => (int) $post1->messageformat,
1295             'messagetrust' => (int) $post1->messagetrust,
1296             'attachment' => $post1->attachment,
1297             'totalscore' => (int) $post1->totalscore,
1298             'mailnow' => (int) $post1->mailnow,
1299             'userfullname' => fullname($user1),
1300             'usermodifiedfullname' => fullname($user4),
1301             'userpictureurl' => '',
1302             'usermodifiedpictureurl' => '',
1303             'numreplies' => 3,
1304             'numunread' => 0,
1305             'pinned' => (bool) FORUM_DISCUSSION_UNPINNED,
1306             'locked' => false,
1307             'canreply' => false,
1308             'canlock' => false,
1309             'starred' => false,
1310             'canfavourite' => true
1311         );
1313         // Call the external function passing forum id.
1314         $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1315         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1316         $expectedreturn = array(
1317             'discussions' => array($expecteddiscussions),
1318             'warnings' => array()
1319         );
1321         // Wait the theme to be loaded (the external_api call does that) to generate the user profiles.
1322         $userpicture = new user_picture($user1);
1323         $userpicture->size = 2; // Size f2.
1324         $expectedreturn['discussions'][0]['userpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1326         $userpicture = new user_picture($user4);
1327         $userpicture->size = 2; // Size f2.
1328         $expectedreturn['discussions'][0]['usermodifiedpictureurl'] = $userpicture->get_url($PAGE)->out(false);
1330         $this->assertEquals($expectedreturn, $discussions);
1332         // Test the starring functionality return.
1333         $t = mod_forum_external::toggle_favourite_state($discussion1->id, 1);
1334         $expectedreturn['discussions'][0]['starred'] = true;
1335         $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1336         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1337         $this->assertEquals($expectedreturn, $discussions);
1339         // Call without required view discussion capability.
1340         $this->unassignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1341         try {
1342             mod_forum_external::get_forum_discussions($forum1->id);
1343             $this->fail('Exception expected due to missing capability.');
1344         } catch (moodle_exception $e) {
1345             $this->assertEquals('noviewdiscussionspermission', $e->errorcode);
1346         }
1348         // Unenrol user from second course.
1349         $enrol->unenrol_user($instance1, $user1->id);
1351         // Call for the second course we unenrolled the user from, make sure exception thrown.
1352         try {
1353             mod_forum_external::get_forum_discussions($forum1->id);
1354             $this->fail('Exception expected due to being unenrolled from the course.');
1355         } catch (moodle_exception $e) {
1356             $this->assertEquals('requireloginerror', $e->errorcode);
1357         }
1359         $this->setAdminUser();
1360         $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1361         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1362         $this->assertTrue($discussions['discussions'][0]['canlock']);
1363     }
1365     /**
1366      * Test the sorting in get forum discussions
1367      */
1368     public function test_mod_forum_get_forum_discussions_sorting() {
1369         global $CFG, $DB, $PAGE;
1371         $this->resetAfterTest(true);
1373         // Set the CFG variable to allow track forums.
1374         $CFG->forum_trackreadposts = true;
1376         // Create a user who can track forums.
1377         $record = new stdClass();
1378         $record->trackforums = true;
1379         $user1 = self::getDataGenerator()->create_user($record);
1380         // Create a bunch of other users to post.
1381         $user2 = self::getDataGenerator()->create_user();
1382         $user3 = self::getDataGenerator()->create_user();
1383         $user4 = self::getDataGenerator()->create_user();
1385         // Set the first created user to the test user.
1386         self::setUser($user1);
1388         // Create courses to add the modules.
1389         $course1 = self::getDataGenerator()->create_course();
1391         // Enrol the user in the first course.
1392         $enrol = enrol_get_plugin('manual');
1394         // We don't use the dataGenerator as we need to get the $instance2 to unenrol later.
1395         $enrolinstances = enrol_get_instances($course1->id, true);
1396         foreach ($enrolinstances as $courseenrolinstance) {
1397             if ($courseenrolinstance->enrol == "manual") {
1398                 $instance1 = $courseenrolinstance;
1399                 break;
1400             }
1401         }
1402         $enrol->enrol_user($instance1, $user1->id);
1404         // First forum with tracking off.
1405         $record = new stdClass();
1406         $record->course = $course1->id;
1407         $record->trackingtype = FORUM_TRACKING_OFF;
1408         $forum1 = self::getDataGenerator()->create_module('forum', $record);
1410         // Assign capabilities to view discussions for forum 1.
1411         $cm = get_coursemodule_from_id('forum', $forum1->cmid, 0, false, MUST_EXIST);
1412         $context = context_module::instance($cm->id);
1413         $newrole = create_role('Role 2', 'role2', 'Role 2 description');
1414         $this->assignUserCapability('mod/forum:viewdiscussion', $context->id, $newrole);
1416         // Add discussions to the forums.
1417         $record = new stdClass();
1418         $record->course = $course1->id;
1419         $record->userid = $user1->id;
1420         $record->forum = $forum1->id;
1421         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1422         sleep(1);
1424         // Add three replies to the discussion 1 from different users.
1425         $record = new stdClass();
1426         $record->discussion = $discussion1->id;
1427         $record->parent = $discussion1->firstpost;
1428         $record->userid = $user2->id;
1429         $discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1430         sleep(1);
1432         $record->parent = $discussion1reply1->id;
1433         $record->userid = $user3->id;
1434         $discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1435         sleep(1);
1437         $record->userid = $user4->id;
1438         $discussion1reply3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
1439         sleep(1);
1441         // Create discussion2.
1442         $record2 = new stdClass();
1443         $record2->course = $course1->id;
1444         $record2->userid = $user1->id;
1445         $record2->forum = $forum1->id;
1446         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record2);
1447         sleep(1);
1449         // Add one reply to the discussion 2.
1450         $record2 = new stdClass();
1451         $record2->discussion = $discussion2->id;
1452         $record2->parent = $discussion2->firstpost;
1453         $record2->userid = $user2->id;
1454         $discussion2reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record2);
1455         sleep(1);
1457         // Create discussion 3.
1458         $record3 = new stdClass();
1459         $record3->course = $course1->id;
1460         $record3->userid = $user1->id;
1461         $record3->forum = $forum1->id;
1462         $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record3);
1463         sleep(1);
1465         // Add two replies to the discussion 3.
1466         $record3 = new stdClass();
1467         $record3->discussion = $discussion3->id;
1468         $record3->parent = $discussion3->firstpost;
1469         $record3->userid = $user2->id;
1470         $discussion3reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1471         sleep(1);
1473         $record3->parent = $discussion3reply1->id;
1474         $record3->userid = $user3->id;
1475         $discussion3reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record3);
1477         // Call the external function passing forum id.
1478         $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1479         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1480         // Discussions should be ordered by last post date in descending order by default.
1481         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1482         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1483         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1485         $vaultfactory = \mod_forum\local\container::get_vault_factory();
1486         $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
1488         // Call the external function passing forum id and sort order parameter.
1489         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1490         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1491         // Discussions should be ordered by last post date in ascending order.
1492         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1493         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1494         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1496         // Call the external function passing forum id and sort order parameter.
1497         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_DESC);
1498         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1499         // Discussions should be ordered by discussion creation date in descending order.
1500         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion3->id);
1501         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1502         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1504         // Call the external function passing forum id and sort order parameter.
1505         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_CREATED_ASC);
1506         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1507         // Discussions should be ordered by discussion creation date in ascending order.
1508         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1509         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion2->id);
1510         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1512         // Call the external function passing forum id and sort order parameter.
1513         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_DESC);
1514         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1515         // Discussions should be ordered by the number of replies in descending order.
1516         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion1->id);
1517         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1518         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion2->id);
1520         // Call the external function passing forum id and sort order parameter.
1521         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_REPLIES_ASC);
1522         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1523         // Discussions should be ordered by the number of replies in ascending order.
1524         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1525         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1526         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1528         // Pin discussion2.
1529         $DB->update_record('forum_discussions',
1530             (object) array('id' => $discussion2->id, 'pinned' => FORUM_DISCUSSION_PINNED));
1532         // Call the external function passing forum id.
1533         $discussions = mod_forum_external::get_forum_discussions($forum1->id);
1534         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1535         // Discussions should be ordered by last post date in descending order by default.
1536         // Pinned discussions should be at the top of the list.
1537         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1538         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion3->id);
1539         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion1->id);
1541         // Call the external function passing forum id and sort order parameter.
1542         $discussions = mod_forum_external::get_forum_discussions($forum1->id, $discussionlistvault::SORTORDER_LASTPOST_ASC);
1543         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
1544         // Discussions should be ordered by last post date in ascending order.
1545         // Pinned discussions should be at the top of the list.
1546         $this->assertEquals($discussions['discussions'][0]['discussion'], $discussion2->id);
1547         $this->assertEquals($discussions['discussions'][1]['discussion'], $discussion1->id);
1548         $this->assertEquals($discussions['discussions'][2]['discussion'], $discussion3->id);
1549     }
1551     /**
1552      * Test add_discussion_post
1553      */
1554     public function test_add_discussion_post() {
1555         global $CFG;
1557         $this->resetAfterTest(true);
1559         $user = self::getDataGenerator()->create_user();
1560         $otheruser = self::getDataGenerator()->create_user();
1562         self::setAdminUser();
1564         // Create course to add the module.
1565         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1567         // Forum with tracking off.
1568         $record = new stdClass();
1569         $record->course = $course->id;
1570         $forum = self::getDataGenerator()->create_module('forum', $record);
1571         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1572         $forumcontext = context_module::instance($forum->cmid);
1574         // Add discussions to the forums.
1575         $record = new stdClass();
1576         $record->course = $course->id;
1577         $record->userid = $user->id;
1578         $record->forum = $forum->id;
1579         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1581         // Try to post (user not enrolled).
1582         self::setUser($user);
1583         try {
1584             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1585             $this->fail('Exception expected due to being unenrolled from the course.');
1586         } catch (moodle_exception $e) {
1587             $this->assertEquals('requireloginerror', $e->errorcode);
1588         }
1590         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1591         $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
1593         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1594         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1596         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1597         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1598         // We receive the discussion and the post.
1599         $this->assertEquals(2, count($posts['posts']));
1601         $tested = false;
1602         foreach ($posts['posts'] as $thispost) {
1603             if ($createdpost['postid'] == $thispost['id']) {
1604                 $this->assertEquals('some subject', $thispost['subject']);
1605                 $this->assertEquals('some text here...', $thispost['message']);
1606                 $this->assertEquals(FORMAT_HTML, $thispost['messageformat']); // This is the default if format was not specified.
1607                 $tested = true;
1608             }
1609         }
1610         $this->assertTrue($tested);
1612         // Let's simulate a call with any other format, it should be stored that way.
1613         global $DB; // Yes, we are going to use DB facilities too, because cannot rely on other functions for checking
1614                     // the format. They eat it completely (going back to FORMAT_HTML. So we only can trust DB for further
1615                     // processing.
1616         $formats = [FORMAT_PLAIN, FORMAT_MOODLE, FORMAT_MARKDOWN, FORMAT_HTML];
1617         $options = [];
1618         foreach ($formats as $format) {
1619             $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1620                 'with some format', 'some formatted here...', $options, $format);
1621             $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1622             $dbformat = $DB->get_field('forum_posts', 'messageformat', ['id' => $createdpost['postid']]);
1623             $this->assertEquals($format, $dbformat);
1624         }
1626         // Now let's try the 'topreferredformat' option. That should end with the content
1627         // transformed and the format being FORMAT_HTML (when, like in this case,  user preferred
1628         // format is HTML, inferred from editor in preferences).
1629         $options = [['name' => 'topreferredformat', 'value' => true]];
1630         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost,
1631             'interesting subject', 'with some https://example.com link', $options, FORMAT_MOODLE);
1632         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1633         $dbpost = $DB->get_record('forum_posts', ['id' => $createdpost['postid']]);
1634         // Format HTML and content converted, we should get.
1635         $this->assertEquals(FORMAT_HTML, $dbpost->messageformat);
1636         $this->assertEquals('<div class="text_to_html">with some https://example.com link</div>', $dbpost->message);
1638         // Test inline and regular attachment in post
1639         // Create a file in a draft area for inline attachments.
1640         $draftidinlineattach = file_get_unused_draft_itemid();
1641         $draftidattach = file_get_unused_draft_itemid();
1642         self::setUser($user);
1643         $usercontext = context_user::instance($user->id);
1644         $filepath = '/';
1645         $filearea = 'draft';
1646         $component = 'user';
1647         $filenameimg = 'shouldbeanimage.txt';
1648         $filerecordinline = array(
1649             'contextid' => $usercontext->id,
1650             'component' => $component,
1651             'filearea'  => $filearea,
1652             'itemid'    => $draftidinlineattach,
1653             'filepath'  => $filepath,
1654             'filename'  => $filenameimg,
1655         );
1656         $fs = get_file_storage();
1658         // Create a file in a draft area for regular attachments.
1659         $filerecordattach = $filerecordinline;
1660         $attachfilename = 'attachment.txt';
1661         $filerecordattach['filename'] = $attachfilename;
1662         $filerecordattach['itemid'] = $draftidattach;
1663         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
1664         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1666         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1667                          array('name' => 'attachmentsid', 'value' => $draftidattach));
1668         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot
1669                      . "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}"
1670                      . '" alt="inlineimage">.';
1671         $createdpost = mod_forum_external::add_discussion_post($discussion->firstpost, 'new post inline attachment',
1672                                                                $dummytext, $options);
1673         $createdpost = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $createdpost);
1675         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
1676         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1677         // We receive the discussion and the post.
1678         // Can't guarantee order of posts during tests.
1679         $postfound = false;
1680         foreach ($posts['posts'] as $thispost) {
1681             if ($createdpost['postid'] == $thispost['id']) {
1682                 $this->assertEquals($createdpost['postid'], $thispost['id']);
1683                 $this->assertEquals($thispost['attachment'], 1, "There should be a non-inline attachment");
1684                 $this->assertCount(1, $thispost['attachments'], "There should be 1 attachment");
1685                 $this->assertEquals($thispost['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1686                 $this->assertContains('pluginfile.php', $thispost['message']);
1687                 $postfound = true;
1688                 break;
1689             }
1690         }
1692         $this->assertTrue($postfound);
1694         // Check not posting in groups the user is not member of.
1695         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1696         groups_add_member($group->id, $otheruser->id);
1698         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1699         $record->forum = $forum->id;
1700         $record->userid = $otheruser->id;
1701         $record->groupid = $group->id;
1702         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1704         try {
1705             mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
1706             $this->fail('Exception expected due to invalid permissions for posting.');
1707         } catch (moodle_exception $e) {
1708             $this->assertEquals('nopostforum', $e->errorcode);
1709         }
1710     }
1712     /**
1713      * Test add_discussion_post and auto subscription to a discussion.
1714      */
1715     public function test_add_discussion_post_subscribe_discussion() {
1716         global $USER;
1718         $this->resetAfterTest(true);
1720         self::setAdminUser();
1722         $user = self::getDataGenerator()->create_user();
1723         $admin = get_admin();
1724         // Create course to add the module.
1725         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1727         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1729         // Forum with tracking off.
1730         $record = new stdClass();
1731         $record->course = $course->id;
1732         $forum = self::getDataGenerator()->create_module('forum', $record);
1733         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
1735         // Add discussions to the forums.
1736         $record = new stdClass();
1737         $record->course = $course->id;
1738         $record->userid = $admin->id;
1739         $record->forum = $forum->id;
1740         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1741         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1743         // Try to post as user.
1744         self::setUser($user);
1745         // Enable auto subscribe discussion.
1746         $USER->autosubscribe = true;
1747         // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled).
1748         mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...');
1750         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
1751         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1752         // We receive the discussion and the post.
1753         $this->assertEquals(2, count($posts['posts']));
1754         // The user should be subscribed to the discussion after adding a discussion post.
1755         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1757         // Disable auto subscribe discussion.
1758         $USER->autosubscribe = false;
1759         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1760         // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled).
1761         mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...');
1763         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
1764         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1765         // We receive the discussion and the post.
1766         $this->assertEquals(3, count($posts['posts']));
1767         // The user should still be subscribed to the discussion after adding a discussion post.
1768         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
1770         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1771         // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled).
1772         mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...');
1774         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
1775         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1776         // We receive the discussion and the post.
1777         $this->assertEquals(2, count($posts['posts']));
1778         // The user should still not be subscribed to the discussion after adding a discussion post.
1779         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1781         // Passing a value for the discussionsubscribe option parameter.
1782         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1783         // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled),
1784         // and the option parameter 'discussionsubscribe' => true in the webservice.
1785         $option = array('name' => 'discussionsubscribe', 'value' => true);
1786         $options[] = $option;
1787         mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...',
1788             $options);
1790         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
1791         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
1792         // We receive the discussion and the post.
1793         $this->assertEquals(3, count($posts['posts']));
1794         // The user should now be subscribed to the discussion after adding a discussion post.
1795         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
1796     }
1798     /*
1799      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
1800      */
1801     public function test_add_discussion() {
1802         global $CFG, $USER;
1803         $this->resetAfterTest(true);
1805         // Create courses to add the modules.
1806         $course = self::getDataGenerator()->create_course();
1808         $user1 = self::getDataGenerator()->create_user();
1809         $user2 = self::getDataGenerator()->create_user();
1811         // First forum with tracking off.
1812         $record = new stdClass();
1813         $record->course = $course->id;
1814         $record->type = 'news';
1815         $forum = self::getDataGenerator()->create_module('forum', $record);
1817         self::setUser($user1);
1818         $this->getDataGenerator()->enrol_user($user1->id, $course->id);
1820         try {
1821             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1822             $this->fail('Exception expected due to invalid permissions.');
1823         } catch (moodle_exception $e) {
1824             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1825         }
1827         self::setAdminUser();
1828         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1829         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1831         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1832         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1834         $this->assertCount(1, $discussions['discussions']);
1835         $this->assertCount(0, $discussions['warnings']);
1837         $this->assertEquals($createddiscussion['discussionid'], $discussions['discussions'][0]['discussion']);
1838         $this->assertEquals(-1, $discussions['discussions'][0]['groupid']);
1839         $this->assertEquals('the subject', $discussions['discussions'][0]['subject']);
1840         $this->assertEquals('some text here...', $discussions['discussions'][0]['message']);
1842         $discussion2pinned = mod_forum_external::add_discussion($forum->id, 'the pinned subject', 'some 2 text here...', -1,
1843                                                                 array('options' => array('name' => 'discussionpinned',
1844                                                                                          'value' => true)));
1845         $discussion3 = mod_forum_external::add_discussion($forum->id, 'the non pinnedsubject', 'some 3 text here...');
1846         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1847         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1848         $this->assertCount(3, $discussions['discussions']);
1849         $this->assertEquals($discussion2pinned['discussionid'], $discussions['discussions'][0]['discussion']);
1851         // Test inline and regular attachment in new discussion
1852         // Create a file in a draft area for inline attachments.
1854         $fs = get_file_storage();
1856         $draftidinlineattach = file_get_unused_draft_itemid();
1857         $draftidattach = file_get_unused_draft_itemid();
1859         $usercontext = context_user::instance($USER->id);
1860         $filepath = '/';
1861         $filearea = 'draft';
1862         $component = 'user';
1863         $filenameimg = 'shouldbeanimage.txt';
1864         $filerecord = array(
1865             'contextid' => $usercontext->id,
1866             'component' => $component,
1867             'filearea'  => $filearea,
1868             'itemid'    => $draftidinlineattach,
1869             'filepath'  => $filepath,
1870             'filename'  => $filenameimg,
1871         );
1873         // Create a file in a draft area for regular attachments.
1874         $filerecordattach = $filerecord;
1875         $attachfilename = 'attachment.txt';
1876         $filerecordattach['filename'] = $attachfilename;
1877         $filerecordattach['itemid'] = $draftidattach;
1878         $fs->create_file_from_string($filerecord, 'image contents (not really)');
1879         $fs->create_file_from_string($filerecordattach, 'simple text attachment');
1881         $dummytext = 'Here is an inline image: <img src="' . $CFG->wwwroot .
1882                     "/draftfile.php/{$usercontext->id}/user/draft/{$draftidinlineattach}/{$filenameimg}" .
1883                     '" alt="inlineimage">.';
1885         $options = array(array('name' => 'inlineattachmentsid', 'value' => $draftidinlineattach),
1886                          array('name' => 'attachmentsid', 'value' => $draftidattach));
1887         $createddiscussion = mod_forum_external::add_discussion($forum->id, 'the inline attachment subject',
1888                                                                 $dummytext, -1, $options);
1889         $createddiscussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $createddiscussion);
1891         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1892         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1894         $this->assertCount(4, $discussions['discussions']);
1895         $this->assertCount(0, $createddiscussion['warnings']);
1896         // Can't guarantee order of posts during tests.
1897         $postfound = false;
1898         foreach ($discussions['discussions'] as $thisdiscussion) {
1899             if ($createddiscussion['discussionid'] == $thisdiscussion['discussion']) {
1900                 $this->assertEquals($thisdiscussion['attachment'], 1, "There should be a non-inline attachment");
1901                 $this->assertCount(1, $thisdiscussion['attachments'], "There should be 1 attachment");
1902                 $this->assertEquals($thisdiscussion['attachments'][0]['filename'], $attachfilename, "There should be 1 attachment");
1903                 $this->assertNotContains('draftfile.php', $thisdiscussion['message']);
1904                 $this->assertContains('pluginfile.php', $thisdiscussion['message']);
1905                 $postfound = true;
1906                 break;
1907             }
1908         }
1910         $this->assertTrue($postfound);
1911     }
1913     /**
1914      * Test adding discussions in a course with gorups
1915      */
1916     public function test_add_discussion_in_course_with_groups() {
1917         global $CFG;
1919         $this->resetAfterTest(true);
1921         // Create course to add the module.
1922         $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
1923         $user = self::getDataGenerator()->create_user();
1924         $this->getDataGenerator()->enrol_user($user->id, $course->id);
1926         // Forum forcing separate gropus.
1927         $record = new stdClass();
1928         $record->course = $course->id;
1929         $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
1931         // Try to post (user not enrolled).
1932         self::setUser($user);
1934         // The user is not enroled in any group, try to post in a forum with separate groups.
1935         try {
1936             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1937             $this->fail('Exception expected due to invalid group permissions.');
1938         } catch (moodle_exception $e) {
1939             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1940         }
1942         try {
1943             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', 0);
1944             $this->fail('Exception expected due to invalid group permissions.');
1945         } catch (moodle_exception $e) {
1946             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1947         }
1949         // Create a group.
1950         $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1952         // Try to post in a group the user is not enrolled.
1953         try {
1954             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1955             $this->fail('Exception expected due to invalid group permissions.');
1956         } catch (moodle_exception $e) {
1957             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1958         }
1960         // Add the user to a group.
1961         groups_add_member($group->id, $user->id);
1963         // Try to post in a group the user is not enrolled.
1964         try {
1965             mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id + 1);
1966             $this->fail('Exception expected due to invalid group.');
1967         } catch (moodle_exception $e) {
1968             $this->assertEquals('cannotcreatediscussion', $e->errorcode);
1969         }
1971         // Nost add the discussion using a valid group.
1972         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...', $group->id);
1973         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1975         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1976         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1978         $this->assertCount(1, $discussions['discussions']);
1979         $this->assertCount(0, $discussions['warnings']);
1980         $this->assertEquals($discussion['discussionid'], $discussions['discussions'][0]['discussion']);
1981         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1983         // Now add a discussions without indicating a group. The function should guess the correct group.
1984         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
1985         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
1987         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
1988         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
1990         $this->assertCount(2, $discussions['discussions']);
1991         $this->assertCount(0, $discussions['warnings']);
1992         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
1993         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
1995         // Enrol the same user in other group.
1996         $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
1997         groups_add_member($group2->id, $user->id);
1999         // Now add a discussions without indicating a group. The function should guess the correct group (the first one).
2000         $discussion = mod_forum_external::add_discussion($forum->id, 'the subject', 'some text here...');
2001         $discussion = external_api::clean_returnvalue(mod_forum_external::add_discussion_returns(), $discussion);
2003         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
2004         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
2006         $this->assertCount(3, $discussions['discussions']);
2007         $this->assertCount(0, $discussions['warnings']);
2008         $this->assertEquals($group->id, $discussions['discussions'][0]['groupid']);
2009         $this->assertEquals($group->id, $discussions['discussions'][1]['groupid']);
2010         $this->assertEquals($group->id, $discussions['discussions'][2]['groupid']);
2012     }
2014     /*
2015      * Test set_lock_state.
2016      */
2017     public function test_set_lock_state() {
2018         global $DB;
2019         $this->resetAfterTest(true);
2021         // Create courses to add the modules.
2022         $course = self::getDataGenerator()->create_course();
2023         $user = self::getDataGenerator()->create_user();
2024         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2026         // First forum with tracking off.
2027         $record = new stdClass();
2028         $record->course = $course->id;
2029         $record->type = 'news';
2030         $forum = self::getDataGenerator()->create_module('forum', $record);
2032         $record = new stdClass();
2033         $record->course = $course->id;
2034         $record->userid = $user->id;
2035         $record->forum = $forum->id;
2036         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2038         // User who is a student.
2039         self::setUser($user);
2040         $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual');
2042         // Only a teacher should be able to lock a discussion.
2043         try {
2044             $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
2045             $this->fail('Exception expected due to missing capability.');
2046         } catch (moodle_exception $e) {
2047             $this->assertEquals('errorcannotlock', $e->errorcode);
2048         }
2050         // Set the lock.
2051         self::setAdminUser();
2052         $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, 0);
2053         $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
2054         $this->assertTrue($result['locked']);
2055         $this->assertNotEquals(0, $result['times']['locked']);
2057         // Unset the lock.
2058         $result = mod_forum_external::set_lock_state($forum->id, $discussion->id, time());
2059         $result = external_api::clean_returnvalue(mod_forum_external::set_lock_state_returns(), $result);
2060         $this->assertFalse($result['locked']);
2061         $this->assertEquals('0', $result['times']['locked']);
2062     }
2064     /*
2065      * Test can_add_discussion. A basic test since all the API functions are already covered by unit tests.
2066      */
2067     public function test_can_add_discussion() {
2068         global $DB;
2069         $this->resetAfterTest(true);
2071         // Create courses to add the modules.
2072         $course = self::getDataGenerator()->create_course();
2074         $user = self::getDataGenerator()->create_user();
2076         // First forum with tracking off.
2077         $record = new stdClass();
2078         $record->course = $course->id;
2079         $record->type = 'news';
2080         $forum = self::getDataGenerator()->create_module('forum', $record);
2082         // User with no permissions to add in a news forum.
2083         self::setUser($user);
2084         $this->getDataGenerator()->enrol_user($user->id, $course->id);
2086         $result = mod_forum_external::can_add_discussion($forum->id);
2087         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2088         $this->assertFalse($result['status']);
2089         $this->assertFalse($result['canpindiscussions']);
2090         $this->assertTrue($result['cancreateattachment']);
2092         // Disable attachments.
2093         $DB->set_field('forum', 'maxattachments', 0, array('id' => $forum->id));
2094         $result = mod_forum_external::can_add_discussion($forum->id);
2095         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2096         $this->assertFalse($result['status']);
2097         $this->assertFalse($result['canpindiscussions']);
2098         $this->assertFalse($result['cancreateattachment']);
2099         $DB->set_field('forum', 'maxattachments', 1, array('id' => $forum->id));    // Enable attachments again.
2101         self::setAdminUser();
2102         $result = mod_forum_external::can_add_discussion($forum->id);
2103         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2104         $this->assertTrue($result['status']);
2105         $this->assertTrue($result['canpindiscussions']);
2106         $this->assertTrue($result['cancreateattachment']);
2107     }
2109     /*
2110      * A basic test to make sure users cannot post to forum after the cutoff date.
2111      */
2112     public function test_can_add_discussion_after_cutoff() {
2113         $this->resetAfterTest(true);
2115         // Create courses to add the modules.
2116         $course = self::getDataGenerator()->create_course();
2118         $user = self::getDataGenerator()->create_user();
2120         // Create a forum with cutoff date set to a past date.
2121         $forum = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'cutoffdate' => time() - 1]);
2123         // User with no mod/forum:canoverridecutoff capability.
2124         self::setUser($user);
2125         $this->getDataGenerator()->enrol_user($user->id, $course->id);
2127         $result = mod_forum_external::can_add_discussion($forum->id);
2128         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2129         $this->assertFalse($result['status']);
2131         self::setAdminUser();
2132         $result = mod_forum_external::can_add_discussion($forum->id);
2133         $result = external_api::clean_returnvalue(mod_forum_external::can_add_discussion_returns(), $result);
2134         $this->assertTrue($result['status']);
2135     }
2137     /**
2138      * Test get forum posts discussions including rating information.
2139      */
2140     public function test_mod_forum_get_forum_discussion_rating_information() {
2141         global $DB, $CFG;
2142         require_once($CFG->dirroot . '/rating/lib.php');
2144         $this->resetAfterTest(true);
2146         $user1 = self::getDataGenerator()->create_user();
2147         $user2 = self::getDataGenerator()->create_user();
2148         $user3 = self::getDataGenerator()->create_user();
2149         $teacher = self::getDataGenerator()->create_user();
2151         // Create course to add the module.
2152         $course = self::getDataGenerator()->create_course();
2154         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2155         $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
2156         $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual');
2157         $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual');
2158         $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual');
2159         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
2161         // Create the forum.
2162         $record = new stdClass();
2163         $record->course = $course->id;
2164         // Set Aggregate type = Average of ratings.
2165         $record->assessed = RATING_AGGREGATE_AVERAGE;
2166         $record->scale = 100;
2167         $forum = self::getDataGenerator()->create_module('forum', $record);
2168         $context = context_module::instance($forum->cmid);
2170         // Add discussion to the forum.
2171         $record = new stdClass();
2172         $record->course = $course->id;
2173         $record->userid = $user1->id;
2174         $record->forum = $forum->id;
2175         $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2177         // Retrieve the first post.
2178         $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2180         // Rate the discussion as user2.
2181         $rating1 = new stdClass();
2182         $rating1->contextid = $context->id;
2183         $rating1->component = 'mod_forum';
2184         $rating1->ratingarea = 'post';
2185         $rating1->itemid = $post->id;
2186         $rating1->rating = 50;
2187         $rating1->scaleid = 100;
2188         $rating1->userid = $user2->id;
2189         $rating1->timecreated = time();
2190         $rating1->timemodified = time();
2191         $rating1->id = $DB->insert_record('rating', $rating1);
2193         // Rate the discussion as user3.
2194         $rating2 = new stdClass();
2195         $rating2->contextid = $context->id;
2196         $rating2->component = 'mod_forum';
2197         $rating2->ratingarea = 'post';
2198         $rating2->itemid = $post->id;
2199         $rating2->rating = 100;
2200         $rating2->scaleid = 100;
2201         $rating2->userid = $user3->id;
2202         $rating2->timecreated = time() + 1;
2203         $rating2->timemodified = time() + 1;
2204         $rating2->id = $DB->insert_record('rating', $rating2);
2206         // Retrieve the rating for the post as student.
2207         $this->setUser($user1);
2208         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2209         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2210         $this->assertCount(1, $posts['ratinginfo']['ratings']);
2211         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2212         $this->assertFalse($posts['ratinginfo']['canviewall']);
2213         $this->assertFalse($posts['ratinginfo']['ratings'][0]['canrate']);
2214         $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2215         $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2217         // Retrieve the rating for the post as teacher.
2218         $this->setUser($teacher);
2219         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2220         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2221         $this->assertCount(1, $posts['ratinginfo']['ratings']);
2222         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canviewaggregate']);
2223         $this->assertTrue($posts['ratinginfo']['canviewall']);
2224         $this->assertTrue($posts['ratinginfo']['ratings'][0]['canrate']);
2225         $this->assertEquals(2, $posts['ratinginfo']['ratings'][0]['count']);
2226         $this->assertEquals(($rating1->rating + $rating2->rating) / 2, $posts['ratinginfo']['ratings'][0]['aggregate']);
2227     }
2229     /**
2230      * Test mod_forum_get_forum_access_information.
2231      */
2232     public function test_mod_forum_get_forum_access_information() {
2233         global $DB;
2235         $this->resetAfterTest(true);
2237         $student = self::getDataGenerator()->create_user();
2238         $course = self::getDataGenerator()->create_course();
2239         // Create the forum.
2240         $record = new stdClass();
2241         $record->course = $course->id;
2242         $forum = self::getDataGenerator()->create_module('forum', $record);
2244         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2245         $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
2247         self::setUser($student);
2248         $result = mod_forum_external::get_forum_access_information($forum->id);
2249         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2251         // Check default values for capabilities.
2252         $enabledcaps = array('canviewdiscussion', 'canstartdiscussion', 'canreplypost', 'canviewrating', 'cancreateattachment',
2253             'canexportownpost', 'cancantogglefavourite', 'candeleteownpost', 'canallowforcesubscribe');
2255         unset($result['warnings']);
2256         foreach ($result as $capname => $capvalue) {
2257             if (in_array($capname, $enabledcaps)) {
2258                 $this->assertTrue($capvalue);
2259             } else {
2260                 $this->assertFalse($capvalue);
2261             }
2262         }
2263         // Now, unassign some capabilities.
2264         unassign_capability('mod/forum:deleteownpost', $studentrole->id);
2265         unassign_capability('mod/forum:allowforcesubscribe', $studentrole->id);
2266         array_pop($enabledcaps);
2267         array_pop($enabledcaps);
2268         accesslib_clear_all_caches_for_unit_testing();
2270         $result = mod_forum_external::get_forum_access_information($forum->id);
2271         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_access_information_returns(), $result);
2272         unset($result['warnings']);
2273         foreach ($result as $capname => $capvalue) {
2274             if (in_array($capname, $enabledcaps)) {
2275                 $this->assertTrue($capvalue);
2276             } else {
2277                 $this->assertFalse($capvalue);
2278             }
2279         }
2280     }
2282     /**
2283      * Test add_discussion_post
2284      */
2285     public function test_add_discussion_post_private() {
2286         global $DB;
2288         $this->resetAfterTest(true);
2290         self::setAdminUser();
2292         // Create course to add the module.
2293         $course = self::getDataGenerator()->create_course();
2295         // Standard forum.
2296         $record = new stdClass();
2297         $record->course = $course->id;
2298         $forum = self::getDataGenerator()->create_module('forum', $record);
2299         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
2300         $forumcontext = context_module::instance($forum->cmid);
2301         $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2303         // Create an enrol users.
2304         $student1 = self::getDataGenerator()->create_user();
2305         $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
2306         $student2 = self::getDataGenerator()->create_user();
2307         $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
2308         $teacher1 = self::getDataGenerator()->create_user();
2309         $this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher');
2310         $teacher2 = self::getDataGenerator()->create_user();
2311         $this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher');
2313         // Add a new discussion to the forum.
2314         self::setUser($student1);
2315         $record = new stdClass();
2316         $record->course = $course->id;
2317         $record->userid = $student1->id;
2318         $record->forum = $forum->id;
2319         $discussion = $generator->create_discussion($record);
2321         // Have the teacher reply privately.
2322         self::setUser($teacher1);
2323         $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [
2324                 [
2325                     'name' => 'private',
2326                     'value' => true,
2327                 ],
2328             ]);
2329         $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
2330         $privatereply = $DB->get_record('forum_posts', array('id' => $post['postid']));
2331         $this->assertEquals($student1->id, $privatereply->privatereplyto);
2332         // Bump the time of the private reply to ensure order.
2333         $privatereply->created++;
2334         $privatereply->modified = $privatereply->created;
2335         $DB->update_record('forum_posts', $privatereply);
2337         // The teacher will receive their private reply.
2338         self::setUser($teacher1);
2339         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2340         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2341         $this->assertEquals(2, count($posts['posts']));
2342         $this->assertTrue($posts['posts'][0]['isprivatereply']);
2344         // Another teacher on the course will also receive the private reply.
2345         self::setUser($teacher2);
2346         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2347         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2348         $this->assertEquals(2, count($posts['posts']));
2349         $this->assertTrue($posts['posts'][0]['isprivatereply']);
2351         // The student will receive the private reply.
2352         self::setUser($student1);
2353         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2354         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2355         $this->assertEquals(2, count($posts['posts']));
2356         $this->assertTrue($posts['posts'][0]['isprivatereply']);
2358         // Another student will not receive the private reply.
2359         self::setUser($student2);
2360         $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
2361         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2362         $this->assertEquals(1, count($posts['posts']));
2363         $this->assertFalse($posts['posts'][0]['isprivatereply']);
2365         // A user cannot reply to a private reply.
2366         self::setUser($teacher2);
2367         $this->expectException('coding_exception');
2368         $post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [
2369                 'options' => [
2370                     'name' => 'private',
2371                     'value' => false,
2372                 ],
2373             ]);
2374     }
2376     /**
2377      * Test trusted text enabled.
2378      */
2379     public function test_trusted_text_enabled() {
2380         global $USER, $CFG;
2382         $this->resetAfterTest(true);
2383         $CFG->enabletrusttext = 1;
2385         $dangeroustext = '<button>Untrusted text</button>';
2386         $cleantext = 'Untrusted text';
2388         // Create courses to add the modules.
2389         $course = self::getDataGenerator()->create_course();
2390         $user1 = self::getDataGenerator()->create_user();
2392         // First forum with tracking off.
2393         $record = new stdClass();
2394         $record->course = $course->id;
2395         $record->type = 'qanda';
2396         $forum = self::getDataGenerator()->create_module('forum', $record);
2397         $context = context_module::instance($forum->cmid);
2399         // Add discussions to the forums.
2400         $discussionrecord = new stdClass();
2401         $discussionrecord->course = $course->id;
2402         $discussionrecord->userid = $user1->id;
2403         $discussionrecord->forum = $forum->id;
2404         $discussionrecord->message = $dangeroustext;
2405         $discussionrecord->messagetrust  = trusttext_trusted($context);
2406         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2408         self::setAdminUser();
2409         $discussionrecord->userid = $USER->id;
2410         $discussionrecord->messagetrust  = trusttext_trusted($context);
2411         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2413         $discussions = mod_forum_external::get_forum_discussions_paginated($forum->id);
2414         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_paginated_returns(), $discussions);
2416         $this->assertCount(2, $discussions['discussions']);
2417         $this->assertCount(0, $discussions['warnings']);
2418         // Admin message is fully trusted.
2419         $this->assertEquals(1, $discussions['discussions'][0]['messagetrust']);
2420         $this->assertEquals($dangeroustext, $discussions['discussions'][0]['message']);
2421         // Student message is not trusted.
2422         $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2423         $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2425         // Get posts now.
2426         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
2427         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2428         // Admin message is fully trusted.
2429         $this->assertEquals(1, $posts['posts'][0]['messagetrust']);
2430         $this->assertEquals($dangeroustext, $posts['posts'][0]['message']);
2432         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
2433         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2434         // Student message is not trusted.
2435         $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2436         $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2437     }
2439     /**
2440      * Test trusted text disabled.
2441      */
2442     public function test_trusted_text_disabled() {
2443         global $USER, $CFG;
2445         $this->resetAfterTest(true);
2446         $CFG->enabletrusttext = 0;
2448         $dangeroustext = '<button>Untrusted text</button>';
2449         $cleantext = 'Untrusted text';
2451         // Create courses to add the modules.
2452         $course = self::getDataGenerator()->create_course();
2453         $user1 = self::getDataGenerator()->create_user();
2455         // First forum with tracking off.
2456         $record = new stdClass();
2457         $record->course = $course->id;
2458         $record->type = 'qanda';
2459         $forum = self::getDataGenerator()->create_module('forum', $record);
2460         $context = context_module::instance($forum->cmid);
2462         // Add discussions to the forums.
2463         $discussionrecord = new stdClass();
2464         $discussionrecord->course = $course->id;
2465         $discussionrecord->userid = $user1->id;
2466         $discussionrecord->forum = $forum->id;
2467         $discussionrecord->message = $dangeroustext;
2468         $discussionrecord->messagetrust = trusttext_trusted($context);
2469         $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2471         self::setAdminUser();
2472         $discussionrecord->userid = $USER->id;
2473         $discussionrecord->messagetrust = trusttext_trusted($context);
2474         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($discussionrecord);
2476         $discussions = mod_forum_external::get_forum_discussions($forum->id);
2477         $discussions = external_api::clean_returnvalue(mod_forum_external::get_forum_discussions_returns(), $discussions);
2479         $this->assertCount(2, $discussions['discussions']);
2480         $this->assertCount(0, $discussions['warnings']);
2481         // Admin message is not trusted because enabletrusttext is disabled.
2482         $this->assertEquals(0, $discussions['discussions'][0]['messagetrust']);
2483         $this->assertEquals($cleantext, $discussions['discussions'][0]['message']);
2484         // Student message is not trusted.
2485         $this->assertEquals(0, $discussions['discussions'][1]['messagetrust']);
2486         $this->assertEquals($cleantext, $discussions['discussions'][1]['message']);
2488         // Get posts now.
2489         $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
2490         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2491         // Admin message is not trusted because enabletrusttext is disabled.
2492         $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2493         $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2495         $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
2496         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
2497         // Student message is not trusted.
2498         $this->assertEquals(0, $posts['posts'][0]['messagetrust']);
2499         $this->assertEquals($cleantext, $posts['posts'][0]['message']);
2500     }
2502     /**
2503      * Test delete a discussion.
2504      */
2505     public function test_delete_post_discussion() {
2506         global $DB;
2507         $this->resetAfterTest(true);
2509         // Setup test data.
2510         $course = $this->getDataGenerator()->create_course();
2511         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2512         $user = $this->getDataGenerator()->create_user();
2513         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2514         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2516         // Add a discussion.
2517         $record = new stdClass();
2518         $record->course = $course->id;
2519         $record->userid = $user->id;
2520         $record->forum = $forum->id;
2521         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2523         $this->setUser($user);
2524         $result = mod_forum_external::delete_post($discussion->firstpost);
2525         $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2526         $this->assertTrue($result['status']);
2527         $this->assertEquals(0, $DB->count_records('forum_posts', array('id' => $discussion->firstpost)));
2528         $this->assertEquals(0, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2529     }
2531     /**
2532      * Test delete a post.
2533      */
2534     public function test_delete_post_post() {
2535         global $DB;
2536         $this->resetAfterTest(true);
2538         // Setup test data.
2539         $course = $this->getDataGenerator()->create_course();
2540         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2541         $user = $this->getDataGenerator()->create_user();
2542         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2543         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2545         // Add a discussion.
2546         $record = new stdClass();
2547         $record->course = $course->id;
2548         $record->userid = $user->id;
2549         $record->forum = $forum->id;
2550         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2551         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2553         // Add a post.
2554         $record = new stdClass();
2555         $record->course = $course->id;
2556         $record->userid = $user->id;
2557         $record->forum = $forum->id;
2558         $record->discussion = $discussion->id;
2559         $record->parent = $parentpost->id;
2560         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2562         $this->setUser($user);
2563         $result = mod_forum_external::delete_post($post->id);
2564         $result = external_api::clean_returnvalue(mod_forum_external::delete_post_returns(), $result);
2565         $this->assertTrue($result['status']);
2566         $this->assertEquals(1, $DB->count_records('forum_posts', array('discussion' => $discussion->id)));
2567         $this->assertEquals(1, $DB->count_records('forum_discussions', array('id' => $discussion->id)));
2568     }
2570     /**
2571      * Test delete a different user post.
2572      */
2573     public function test_delete_post_other_user_post() {
2574         global $DB;
2575         $this->resetAfterTest(true);
2577         // Setup test data.
2578         $course = $this->getDataGenerator()->create_course();
2579         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
2580         $user = $this->getDataGenerator()->create_user();
2581         $otheruser = $this->getDataGenerator()->create_user();
2582         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
2583         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
2584         self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
2586         // Add a discussion.
2587         $record = array();
2588         $record['course'] = $course->id;
2589         $record['forum'] = $forum->id;
2590         $record['userid'] = $user->id;
2591         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
2592         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
2594         // Add a post.
2595         $record = new stdClass();
2596         $record->course = $course->id;
2597         $record->userid = $user->id;
2598         $record->forum = $forum->id;
2599         $record->discussion = $discussion->id;
2600         $record->parent = $parentpost->id;
2601         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
2603         $this->setUser($otheruser);
2604         $this->expectExceptionMessage(get_string('cannotdeletepost', 'forum'));
2605         mod_forum_external::delete_post($post->id);
2606     }
2608     /*
2609      * Test get forum posts by user id.
2610      */
2611     public function test_mod_forum_get_discussion_posts_by_userid() {
2612         $this->resetAfterTest(true);
2614         $urlfactory = mod_forum\local\container::get_url_factory();
2615         $entityfactory = mod_forum\local\container::get_entity_factory();
2616         $vaultfactory = mod_forum\local\container::get_vault_factory();
2617         $postvault = $vaultfactory->get_post_vault();
2618         $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
2619         $legacypostmapper = $legacydatamapper->get_post_data_mapper();
2621         $user1 = self::getDataGenerator()->create_user();
2622         $user1entity = $entityfactory->get_author_from_stdclass($user1);
2623         $exporteduser1 = [
2624             'id' => (int) $user1->id,
2625             'fullname' => fullname($user1),
2626             'groups' => [],
2627             'urls' => [
2628                 'profile' => $urlfactory->get_author_profile_url($user1entity),
2629                 'profileimage' => $urlfactory->get_author_profile_image_url($user1entity),
2630             ],
2631             'isdeleted' => false,
2632         ];
2633         // Create a bunch of other users to post.
2634         $user2 = self::getDataGenerator()->create_user();
2635         $user2entity = $entityfactory->get_author_from_stdclass($user2);
2636         $exporteduser2 = [
2637             'id' => (int) $user2->id,
2638             'fullname' => fullname($user2),
2639             'groups' => [],
2640             'urls' => [
2641                 'profile' => $urlfactory->get_author_profile_url($user2entity),
2642                 'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
2643             ],
2644             'isdeleted' => false,
2645         ];
2646         $user2->fullname = $exporteduser2['fullname'];
2648         $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
2650         // Set the first created user to the test user.
2651         self::setUser($user1);
2653         // Create course to add the module.
2654         $course1 = self::getDataGenerator()->create_course();
2656         // Forum with tracking off.
2657         $record = new stdClass();
2658         $record->course = $course1->id;
2659         $forum1 = self::getDataGenerator()->create_module('forum', $record);
2660         $forum1context = context_module::instance($forum1->cmid);
2662         // Add discussions to the forums.
2663         $record = new stdClass();
2664         $record->course = $course1->id;
2665         $record->userid = $user1->id;
2666         $record->forum = $forum1->id;
2667         $discussion1 = $forumgenerator->create_discussion($record);
2668         $discussion1firstpost = $postvault->get_first_post_for_discussion_ids([$discussion1->id]);
2669         $discussion1firstpostobject = $legacypostmapper->to_legacy_object($discussion1firstpost[$discussion1->firstpost]);
2671         $record = new stdClass();
2672         $record->course = $course1->id;
2673         $record->userid = $user1->id;
2674         $record->forum = $forum1->id;
2675         $discussion2 = $forumgenerator->create_discussion($record);
2676         $discussion2firstpost = $postvault->get_first_post_for_discussion_ids([$discussion2->id]);
2677         $discussion2firstpostobject = $legacypostmapper->to_legacy_object($discussion2firstpost[$discussion2->firstpost]);
2679         // Add 1 reply to the discussion 1 from a different user.
2680         $record = new stdClass();
2681         $record->discussion = $discussion1->id;
2682         $record->parent = $discussion1->firstpost;
2683         $record->userid = $user2->id;
2684         $discussion1reply1 = $forumgenerator->create_post($record);
2685         $filename = 'shouldbeanimage.jpg';
2686         // Add a fake inline image to the post.
2687         $filerecordinline = array(
2688                 'contextid' => $forum1context->id,
2689                 'component' => 'mod_forum',
2690                 'filearea'  => 'post',
2691                 'itemid'    => $discussion1reply1->id,
2692                 'filepath'  => '/',
2693                 'filename'  => $filename,
2694         );
2695         $fs = get_file_storage();
2696         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2698         // Add 1 reply to the discussion 2 from a different user.
2699         $record = new stdClass();
2700         $record->discussion = $discussion2->id;
2701         $record->parent = $discussion2->firstpost;
2702         $record->userid = $user2->id;
2703         $discussion2reply1 = $forumgenerator->create_post($record);
2704         $filename = 'shouldbeanimage.jpg';
2705         // Add a fake inline image to the post.
2706         $filerecordinline = array(
2707                 'contextid' => $forum1context->id,
2708                 'component' => 'mod_forum',
2709                 'filearea'  => 'post',
2710                 'itemid'    => $discussion2reply1->id,
2711                 'filepath'  => '/',
2712                 'filename'  => $filename,
2713         );
2714         $fs = get_file_storage();
2715         $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2717         // Following line enrol and assign default role id to the user.
2718         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
2719         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
2720         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
2722         // Create what we expect to be returned when querying the discussion.
2723         $expectedposts = array(
2724             'discussions' => array(),
2725             'warnings' => array(),
2726         );
2728         $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
2729         $isolatedurluser->params(['parent' => $discussion1reply1->id]);
2730         $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1firstpostobject->discussion);
2731         $isolatedurlparent->params(['parent' => $discussion1firstpostobject->id]);
2733         $expectedposts['discussions'][0] = [
2734             'name' => $discussion1->name,
2735             'id' => $discussion1->id,
2736             'posts' => [
2737                 'userposts' => [
2738                     [
2739                         'id' => $discussion1reply1->id,
2740                         'discussionid' => $discussion1reply1->discussion,
2741                         'parentid' => $discussion1reply1->parent,
2742                         'hasparent' => true,
2743                         'timecreated' => $discussion1reply1->created,
2744                         'subject' => $discussion1reply1->subject,
2745                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
2746                         'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
2747                         $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
2748                         'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2749                         'unread' => null,
2750                         'isdeleted' => false,
2751                         'isprivatereply' => false,
2752                         'haswordcount' => false,
2753                         'wordcount' => null,
2754                         'author' => $exporteduser2,
2755                         'attachments' => [],
2756                         'tags' => [],
2757                         'html' => [
2758                             'rating' => null,
2759                             'taglist' => null,
2760                             'authorsubheading' => $forumgenerator->get_author_subheading_html(
2761                                 (object)$exporteduser2, $discussion1reply1->created)
2762                         ],
2763                         'charcount' => null,
2764                         'capabilities' => [
2765                             'view' => true,
2766                             'edit' => true,
2767                             'delete' => true,
2768                             'split' => false,
2769                             'reply' => true,
2770                             'export' => false,
2771                             'controlreadstatus' => false,
2772                             'canreplyprivately' => false,
2773                             'selfenrol' => false
2774                         ],
2775                         'urls' => [
2776                             'view' => $urlfactory->get_view_post_url_from_post_id(
2777                                     $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
2778                             'viewisolated' => $isolatedurluser->out(false),
2779                             'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2780                                     $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
2781                             'edit' => (new moodle_url('/mod/forum/post.php', [
2782                                     'edit' => $discussion1reply1->id
2783                             ]))->out(false),
2784                             'delete' => (new moodle_url('/mod/forum/post.php', [
2785                                     'delete' => $discussion1reply1->id
2786                             ]))->out(false),
2787                             'split' => null,
2788                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2789                                     'reply' => $discussion1reply1->id
2790                             ]))->out(false),
2791                             'export' => null,
2792                             'markasread' => null,
2793                             'markasunread' => null,
2794                             'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2795                                     $discussion1reply1->discussion)->out(false),
2796                         ],
2797                     ]
2798                 ],
2799                 'parentposts' => [
2800                     [
2801                         'id' => $discussion1firstpostobject->id,
2802                         'discussionid' => $discussion1firstpostobject->discussion,
2803                         'parentid' => null,
2804                         'hasparent' => false,
2805                         'timecreated' => $discussion1firstpostobject->created,
2806                         'subject' => $discussion1firstpostobject->subject,
2807                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
2808                         'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
2809                             $forum1context->id, 'mod_forum', 'post', $discussion1firstpostobject->id),
2810                         'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2811                         'unread' => null,
2812                         'isdeleted' => false,
2813                         'isprivatereply' => false,
2814                         'haswordcount' => false,
2815                         'wordcount' => null,
2816                         'author' => $exporteduser1,
2817                         'attachments' => [],
2818                         'tags' => [],
2819                         'html' => [
2820                             'rating' => null,
2821                             'taglist' => null,
2822                             'authorsubheading' => $forumgenerator->get_author_subheading_html(
2823                                 (object)$exporteduser1, $discussion1firstpostobject->created)
2824                         ],
2825                         'charcount' => null,
2826                         'capabilities' => [
2827                             'view' => true,
2828                             'edit' => false,
2829                             'delete' => false,
2830                             'split' => false,
2831                             'reply' => true,
2832                             'export' => false,
2833                             'controlreadstatus' => false,
2834                             'canreplyprivately' => false,
2835                             'selfenrol' => false
2836                         ],
2837                         'urls' => [
2838                             'view' => $urlfactory->get_view_post_url_from_post_id(
2839                                 $discussion1firstpostobject->discussion, $discussion1firstpostobject->id)->out(false),
2840                             'viewisolated' => $isolatedurlparent->out(false),
2841                             'viewparent' => null,
2842                             'edit' => null,
2843                             'delete' => null,
2844                             'split' => null,
2845                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2846                                 'reply' => $discussion1firstpostobject->id
2847                             ]))->out(false),
2848                             'export' => null,
2849                             'markasread' => null,
2850                             'markasunread' => null,
2851                             'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2852                                 $discussion1firstpostobject->discussion)->out(false),
2853                         ],
2854                     ]
2855                 ],
2856             ],
2857         ];
2859         $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2reply1->discussion);
2860         $isolatedurluser->params(['parent' => $discussion2reply1->id]);
2861         $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2firstpostobject->discussion);
2862         $isolatedurlparent->params(['parent' => $discussion2firstpostobject->id]);
2864         $expectedposts['discussions'][1] = [
2865             'name' => $discussion2->name,
2866             'id' => $discussion2->id,
2867             'posts' => [
2868                 'userposts' => [
2869                     [
2870                         'id' => $discussion2reply1->id,
2871                         'discussionid' => $discussion2reply1->discussion,
2872                         'parentid' => $discussion2reply1->parent,
2873                         'hasparent' => true,
2874                         'timecreated' => $discussion2reply1->created,
2875                         'subject' => $discussion2reply1->subject,
2876                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
2877                         'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
2878                             $forum1context->id, 'mod_forum', 'post', $discussion2reply1->id),
2879                         'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2880                         'unread' => null,
2881                         'isdeleted' => false,
2882                         'isprivatereply' => false,
2883                         'haswordcount' => false,
2884                         'wordcount' => null,
2885                         'author' => $exporteduser2,
2886                         'attachments' => [],
2887                         'tags' => [],
2888                         'html' => [
2889                             'rating' => null,
2890                             'taglist' => null,
2891                             'authorsubheading' => $forumgenerator->get_author_subheading_html(
2892                                 (object)$exporteduser2, $discussion2reply1->created)
2893                         ],
2894                         'charcount' => null,
2895                         'capabilities' => [
2896                             'view' => true,
2897                             'edit' => true,
2898                             'delete' => true,
2899                             'split' => false,
2900                             'reply' => true,
2901                             'export' => false,
2902                             'controlreadstatus' => false,
2903                             'canreplyprivately' => false,
2904                             'selfenrol' => false
2905                         ],
2906                         'urls' => [
2907                             'view' => $urlfactory->get_view_post_url_from_post_id(
2908                                 $discussion2reply1->discussion, $discussion2reply1->id)->out(false),
2909                             'viewisolated' => $isolatedurluser->out(false),
2910                             'viewparent' => $urlfactory->get_view_post_url_from_post_id(
2911                                 $discussion2reply1->discussion, $discussion2reply1->parent)->out(false),
2912                             'edit' => (new moodle_url('/mod/forum/post.php', [
2913                                 'edit' => $discussion2reply1->id
2914                             ]))->out(false),
2915                             'delete' => (new moodle_url('/mod/forum/post.php', [
2916                                 'delete' => $discussion2reply1->id
2917                             ]))->out(false),
2918                             'split' => null,
2919                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2920                                 'reply' => $discussion2reply1->id
2921                             ]))->out(false),
2922                             'export' => null,
2923                             'markasread' => null,
2924                             'markasunread' => null,
2925                             'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2926                                 $discussion2reply1->discussion)->out(false),
2927                         ],
2928                     ]
2929                 ],
2930                 'parentposts' => [
2931                     [
2932                         'id' => $discussion2firstpostobject->id,
2933                         'discussionid' => $discussion2firstpostobject->discussion,
2934                         'parentid' => null,
2935                         'hasparent' => false,
2936                         'timecreated' => $discussion2firstpostobject->created,
2937                         'subject' => $discussion2firstpostobject->subject,
2938                         'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
2939                         'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
2940                             $forum1context->id, 'mod_forum', 'post', $discussion2firstpostobject->id),
2941                         'messageformat' => 1,   // This value is usually changed by external_format_text() function.
2942                         'unread' => null,
2943                         'isdeleted' => false,
2944                         'isprivatereply' => false,
2945                         'haswordcount' => false,
2946                         'wordcount' => null,
2947                         'author' => $exporteduser1,
2948                         'attachments' => [],
2949                         'tags' => [],
2950                         'html' => [
2951                             'rating' => null,
2952                             'taglist' => null,
2953                             'authorsubheading' => $forumgenerator->get_author_subheading_html(
2954                                 (object)$exporteduser1, $discussion2firstpostobject->created)
2955                         ],
2956                         'charcount' => null,
2957                         'capabilities' => [
2958                             'view' => true,
2959                             'edit' => false,
2960                             'delete' => false,
2961                             'split' => false,
2962                             'reply' => true,
2963                             'export' => false,
2964                             'controlreadstatus' => false,
2965                             'canreplyprivately' => false,
2966                             'selfenrol' => false
2967                         ],
2968                         'urls' => [
2969                             'view' => $urlfactory->get_view_post_url_from_post_id(
2970                                 $discussion2firstpostobject->discussion, $discussion2firstpostobject->id)->out(false),
2971                             'viewisolated' => $isolatedurlparent->out(false),
2972                             'viewparent' => null,
2973                             'edit' => null,
2974                             'delete' => null,
2975                             'split' => null,
2976                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
2977                                 'reply' => $discussion2firstpostobject->id
2978                             ]))->out(false),
2979                             'export' => null,
2980                             'markasread' => null,
2981                             'markasunread' => null,
2982                             'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
2983                                 $discussion2firstpostobject->discussion)->out(false),
2985                         ]
2986                     ],
2987                 ]
2988             ],
2989         ];
2991         // Test discussions with one additional post each (total 2 posts).
2992         // Also testing that we get the parent posts too.
2993         $discussions = mod_forum_external::get_discussion_posts_by_userid($user2->id, $forum1->cmid, 'modified', 'DESC');
2994         $discussions = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_by_userid_returns(), $discussions);
2996         $this->assertEquals(2, count($discussions['discussions']));
2998         $this->assertEquals($expectedposts, $discussions);
2999     }
3001     /**
3002      * Test get_discussion_post a discussion.
3003      */
3004     public function test_get_discussion_post_discussion() {
3005         global $DB;
3006         $this->resetAfterTest(true);
3007         // Setup test data.
3008         $course = $this->getDataGenerator()->create_course();
3009         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3010         $user = $this->getDataGenerator()->create_user();
3011         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3012         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3013         // Add a discussion.
3014         $record = new stdClass();
3015         $record->course = $course->id;
3016         $record->userid = $user->id;
3017         $record->forum = $forum->id;
3018         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3019         $this->setUser($user);
3020         $result = mod_forum_external::get_discussion_post($discussion->firstpost);
3021         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3022         $this->assertEquals($discussion->firstpost, $result['post']['id']);
3023         $this->assertFalse($result['post']['hasparent']);
3024         $this->assertEquals($discussion->message, $result['post']['message']);
3025     }
3027     /**
3028      * Test get_discussion_post a post.
3029      */
3030     public function test_get_discussion_post_post() {
3031         global $DB;
3032         $this->resetAfterTest(true);
3033         // Setup test data.
3034         $course = $this->getDataGenerator()->create_course();
3035         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3036         $user = $this->getDataGenerator()->create_user();
3037         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3038         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3039         // Add a discussion.
3040         $record = new stdClass();
3041         $record->course = $course->id;
3042         $record->userid = $user->id;
3043         $record->forum = $forum->id;
3044         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3045         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3046         // Add a post.
3047         $record = new stdClass();
3048         $record->course = $course->id;
3049         $record->userid = $user->id;
3050         $record->forum = $forum->id;
3051         $record->discussion = $discussion->id;
3052         $record->parent = $parentpost->id;
3053         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3054         $this->setUser($user);
3055         $result = mod_forum_external::get_discussion_post($post->id);
3056         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3057         $this->assertEquals($post->id, $result['post']['id']);
3058         $this->assertTrue($result['post']['hasparent']);
3059         $this->assertEquals($post->message, $result['post']['message']);
3060     }
3062     /**
3063      * Test get_discussion_post a different user post.
3064      */
3065     public function test_get_discussion_post_other_user_post() {
3066         global $DB;
3067         $this->resetAfterTest(true);
3068         // Setup test data.
3069         $course = $this->getDataGenerator()->create_course();
3070         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3071         $user = $this->getDataGenerator()->create_user();
3072         $otheruser = $this->getDataGenerator()->create_user();
3073         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3074         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3075         self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
3076         // Add a discussion.
3077         $record = array();
3078         $record['course'] = $course->id;
3079         $record['forum'] = $forum->id;
3080         $record['userid'] = $user->id;
3081         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3082         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3083         // Add a post.
3084         $record = new stdClass();
3085         $record->course = $course->id;
3086         $record->userid = $user->id;
3087         $record->forum = $forum->id;
3088         $record->discussion = $discussion->id;
3089         $record->parent = $parentpost->id;
3090         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3091         // Check other user post.
3092         $this->setUser($otheruser);
3093         $result = mod_forum_external::get_discussion_post($post->id);
3094         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3095         $this->assertEquals($post->id, $result['post']['id']);
3096         $this->assertTrue($result['post']['hasparent']);
3097         $this->assertEquals($post->message, $result['post']['message']);
3098     }
3100     /**
3101      * Test prepare_draft_area_for_post a different user post.
3102      */
3103     public function test_prepare_draft_area_for_post() {
3104         global $DB;
3105         $this->resetAfterTest(true);
3106         // Setup test data.
3107         $course = $this->getDataGenerator()->create_course();
3108         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3109         $user = $this->getDataGenerator()->create_user();
3110         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3111         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3112         // Add a discussion.
3113         $record = array();
3114         $record['course'] = $course->id;
3115         $record['forum'] = $forum->id;
3116         $record['userid'] = $user->id;
3117         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3118         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
3119         // Add a post.
3120         $record = new stdClass();
3121         $record->course = $course->id;
3122         $record->userid = $user->id;
3123         $record->forum = $forum->id;
3124         $record->discussion = $discussion->id;
3125         $record->parent = $parentpost->id;
3126         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3128         // Add some files only in the attachment area.
3129         $filename = 'faketxt.txt';
3130         $filerecordinline = array(
3131             'contextid' => context_module::instance($forum->cmid)->id,
3132             'component' => 'mod_forum',
3133             'filearea'  => 'attachment',
3134             'itemid'    => $post->id,
3135             'filepath'  => '/',
3136             'filename'  => $filename,
3137         );
3138         $fs = get_file_storage();
3139         $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
3140         $filerecordinline['filename'] = 'otherfaketxt.txt';
3141         $fs->create_file_from_string($filerecordinline, 'fake txt contents 2.');
3143         $this->setUser($user);
3145         // Check attachment area.
3146         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment');
3147         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3148         $this->assertCount(2, $result['files']);
3149         $this->assertEquals($filename, $result['files'][0]['filename']);
3150         $this->assertCount(5, $result['areaoptions']);
3151         $this->assertEmpty($result['messagetext']);
3153         // Check again using existing draft item id.
3154         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid']);
3155         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3156         $this->assertCount(2, $result['files']);
3158         // Keep only certain files in the area.
3159         $filestokeep = array(array('filename' => $filename, 'filepath' => '/'));
3160         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid'], $filestokeep);
3161         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3162         $this->assertCount(1, $result['files']);
3163         $this->assertEquals($filename, $result['files'][0]['filename']);
3165         // Check editor (post) area.
3166         $filerecordinline['filearea'] = 'post';
3167         $filerecordinline['filename'] = 'fakeimage.png';
3168         $fs->create_file_from_string($filerecordinline, 'fake image.');
3169         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'post');
3170         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
3171         $this->assertCount(1, $result['files']);
3172         $this->assertEquals($filerecordinline['filename'], $result['files'][0]['filename']);
3173         $this->assertCount(5, $result['areaoptions']);
3174         $this->assertEquals($post->message, $result['messagetext']);
3175     }
3177     /**
3178      * Test update_discussion_post with a discussion.
3179      */
3180     public function test_update_discussion_post_discussion() {
3181         global $DB, $USER;
3182         $this->resetAfterTest(true);
3183         // Setup test data.
3184         $course = $this->getDataGenerator()->create_course();
3185         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3187         $this->setAdminUser();
3189         // Add a discussion.
3190         $record = new stdClass();
3191         $record->course = $course->id;
3192         $record->userid = $USER->id;
3193         $record->forum = $forum->id;
3194         $record->pinned = FORUM_DISCUSSION_UNPINNED;
3195         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3197         $subject = 'Hey subject updated';
3198         $message = 'Hey message updated';
3199         $messageformat = FORMAT_HTML;
3200         $options = [
3201             ['name' => 'pinned', 'value' => true],
3202         ];
3204         $result = mod_forum_external::update_discussion_post($discussion->firstpost, $subject, $message, $messageformat,
3205             $options);
3206         $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3207         $this->assertTrue($result['status']);
3209         // Get the post from WS.
3210         $result = mod_forum_external::get_discussion_post($discussion->firstpost);
3211         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
3212         $this->assertEquals($subject, $result['post']['subject']);
3213         $this->assertEquals($message, $result['post']['message']);
3214         $this->assertEquals($messageformat, $result['post']['messageformat']);
3216         // Get discussion object from DB.
3217         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
3218         $this->assertEquals($subject, $discussion->name);   // Check discussion subject.
3219         $this->assertEquals(FORUM_DISCUSSION_PINNED, $discussion->pinned);  // Check discussion pinned.
3220     }
3222     /**
3223      * Test update_discussion_post with a post.
3224      */
3225     public function test_update_discussion_post_post() {
3226         global $DB, $USER;
3227         $this->resetAfterTest(true);
3228         // Setup test data.
3229         $course = $this->getDataGenerator()->create_course();
3230         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3231         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
3232         $user = $this->getDataGenerator()->create_user();
3233         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3234         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3236         $this->setUser($user);
3237         // Enable auto subscribe discussion.
3238         $USER->autosubscribe = true;
3240         // Add a discussion.
3241         $record = new stdClass();
3242         $record->course = $course->id;
3243         $record->userid = $user->id;
3244         $record->forum = $forum->id;
3245         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3247         // Add a post via WS (so discussion subscription works).
3248         $result = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
3249         $newpost = $result['post'];
3250         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3252         // Add files in the different areas.
3253         $draftidattach = file_get_unused_draft_itemid();
3254         $filerecordinline = array(
3255             'contextid' => context_user::instance($user->id)->id,
3256             'component' => 'user',
3257             'filearea'  => 'draft',
3258             'itemid'    => $draftidattach,
3259             'filepath'  => '/',
3260             'filename'  => 'faketxt.txt',
3261         );
3262         $fs = get_file_storage();
3263         $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
3265         // Create files in post area (inline).
3266         $draftidinlineattach = file_get_unused_draft_itemid();
3267         $filerecordinline['itemid'] = $draftidinlineattach;
3268         $filerecordinline['filename'] = 'fakeimage.png';
3269         $fs->create_file_from_string($filerecordinline, 'img...');
3271         // Do not update subject.
3272         $message = 'Hey message updated';
3273         $messageformat = FORMAT_HTML;
3274         $options = [
3275             ['name' => 'discussionsubscribe', 'value' => false],
3276             ['name' => 'inlineattachmentsid', 'value' => $draftidinlineattach],
3277             ['name' => 'attachmentsid', 'value' => $draftidattach],
3278         ];
3280         $result = mod_forum_external::update_discussion_post($newpost->id, '', $message, $messageformat, $options);
3281         $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
3282         $this->assertTrue($result['status']);
3283         // Check subscription status.
3284         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
3286         // Get the post from WS.
3287         $result = mod_forum_external::get_forum_discussion_posts($discussion->id);
3288         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $result);
3289         $found = false;
3290         foreach ($result['posts'] as $post) {
3291             if ($post['id'] == $newpost->id) {
3292                 $this->assertEquals($newpost->subject, $post['subject']);
3293                 $this->assertEquals($message, $post['message']);
3294                 $this->assertEquals($messageformat, $post['messageformat']);
3295                 $this->assertCount(1, $post['messageinlinefiles']);
3296                 $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']);
3297                 $this->assertCount(1, $post['attachments']);
3298                 $this->assertEquals('faketxt.txt', $post['attachments'][0]['filename']);
3299                 $found = true;
3300             }
3301         }
3302         $this->assertTrue($found);
3303     }
3305     /**
3306      * Test update_discussion_post with other user post (no permissions).
3307      */
3308     public function test_update_discussion_post_other_user_post() {
3309         global $DB, $USER;
3310         $this->resetAfterTest(true);
3311         // Setup test data.
3312         $course = $this->getDataGenerator()->create_course();
3313         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
3314         $user = $this->getDataGenerator()->create_user();
3315         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
3316         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
3318         $this->setAdminUser();
3319         // Add a discussion.
3320         $record = new stdClass();
3321         $record->course = $course->id;
3322         $record->userid = $USER->id;
3323         $record->forum = $forum->id;
3324         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
3326         // Add a post.
3327         $record = new stdClass();
3328         $record->course = $course->id;
3329         $record->userid = $USER->id;
3330         $record->forum = $forum->id;
3331         $record->discussion = $discussion->id;
3332         $record->parent = $discussion->firstpost;
3333         $newpost = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
3335         $this->setUser($user);
3336         $subject = 'Hey subject updated';
3337         $message = 'Hey message updated';
3338         $messageformat = FORMAT_HTML;
3340         $this->expectExceptionMessage(get_string('cannotupdatepost', 'forum'));
3341         mod_forum_external::update_discussion_post($newpost->id, $subject, $message, $messageformat);
3342     }