Merge branch 'MDL-64254-master' of git://github.com/jleyva/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 30 Oct 2019 06:33:01 +0000 (14:33 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 30 Oct 2019 06:35:42 +0000 (14:35 +0800)
1  2 
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/forum/version.php

@@@ -185,14 -183,28 +185,38 @@@ $functions = array
          'type' => 'write',
          'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
      ),
 +
 +    'mod_forum_get_discussion_posts_by_userid' => array(
 +        'classname' => 'mod_forum_external',
 +        'methodname' => 'get_discussion_posts_by_userid',
 +        'classpath' => 'mod/forum/externallib.php',
 +        'description' => 'Returns a list of forum posts for a discussion for a user.',
 +        'type' => 'read',
 +        'ajax' => true,
 +        'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
 +    ),
+     'mod_forum_get_discussion_post' => array(
+         'classname' => 'mod_forum_external',
+         'methodname' => 'get_discussion_post',
+         'classpath' => 'mod/forum/externallib.php',
+         'description' => 'Get a particular discussion post.',
+         'type' => 'read',
+         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+     ),
+     'mod_forum_prepare_draft_area_for_post' => array(
+         'classname' => 'mod_forum_external',
+         'methodname' => 'prepare_draft_area_for_post',
+         'classpath' => 'mod/forum/externallib.php',
+         'description' => 'Prepares a draft area for editing a post.',
+         'type' => 'write',
+         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+     ),
+     'mod_forum_update_discussion_post' => array(
+         'classname' => 'mod_forum_external',
+         'methodname' => 'update_discussion_post',
+         'classpath' => 'mod/forum/externallib.php',
+         'description' => 'Updates a post or a discussion topic post.',
+         'type' => 'write',
+         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+     ),
  );
@@@ -2156,140 -2153,449 +2156,590 @@@ class mod_forum_external extends extern
          );
      }
  
 +    /**
 +     * Get the forum posts in the specified forum instance.
 +     *
 +     * @param   int $userid
 +     * @param   int $cmid
 +     * @param   string $sortby
 +     * @param   string $sortdirection
 +     * @return  array
 +     */
 +    public static function get_discussion_posts_by_userid(int $userid = 0, int $cmid, ?string $sortby, ?string $sortdirection) {
 +        global $USER, $DB;
 +        // Validate the parameter.
 +        $params = self::validate_parameters(self::get_discussion_posts_by_userid_parameters(), [
 +                'userid' => $userid,
 +                'cmid' => $cmid,
 +                'sortby' => $sortby,
 +                'sortdirection' => $sortdirection,
 +        ]);
 +        $warnings = [];
 +
 +        $user = core_user::get_user($params['userid']);
 +
 +        $vaultfactory = mod_forum\local\container::get_vault_factory();
 +
 +        $forumvault = $vaultfactory->get_forum_vault();
 +        $forum = $forumvault->get_from_course_module_id($params['cmid']);
 +
 +        // Validate the module context. It checks everything that affects the module visibility (including groupings, etc..).
 +        self::validate_context($forum->get_context());
 +
 +        $sortby = $params['sortby'];
 +        $sortdirection = $params['sortdirection'];
 +        $sortallowedvalues = ['id', 'created', 'modified'];
 +        $directionallowedvalues = ['ASC', 'DESC'];
 +
 +        if (!in_array(strtolower($sortby), $sortallowedvalues)) {
 +            throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $sortby . '),' .
 +                    'allowed values are: ' . implode(', ', $sortallowedvalues));
 +        }
 +
 +        $sortdirection = strtoupper($sortdirection);
 +        if (!in_array($sortdirection, $directionallowedvalues)) {
 +            throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
 +                    'allowed values are: ' . implode(',', $directionallowedvalues));
 +        }
 +
 +        $managerfactory = mod_forum\local\container::get_manager_factory();
 +        $capabilitymanager = $managerfactory->get_capability_manager($forum);
 +
 +        $discussionvault = $vaultfactory->get_discussion_vault();
 +        $discussions = $discussionvault->get_all_discussions_in_forum($forum, 'timemodified ASC, id ASC');
 +
 +        $postvault = $vaultfactory->get_post_vault();
 +
 +        $builderfactory = mod_forum\local\container::get_builder_factory();
 +        $postbuilder = $builderfactory->get_exported_posts_builder();
 +
 +        $builtdiscussions = [];
 +        foreach ($discussions as $id => $discussion) {
 +            $posts = $postvault->get_posts_in_discussion_for_user_id(
 +                    $discussion->get_id(),
 +                    $user->id,
 +                    $capabilitymanager->can_view_any_private_reply($USER),
 +                    "{$sortby} {$sortdirection}"
 +            );
 +            if (empty($posts)) {
 +                continue;
 +            }
 +
 +            $parentids = array_filter(array_map(function($post) {
 +                return $post->has_parent() ? $post->get_parent_id() : null;
 +            }, $posts));
 +
 +            $parentposts = [];
 +            if ($parentids) {
 +                $parentposts = $postbuilder->build(
 +                    $user,
 +                    [$forum],
 +                    [$discussion],
 +                    $postvault->get_from_ids(array_values($parentids))
 +                );
 +            }
 +
 +            $builtdiscussions[] = [
 +                'name' => $discussion->get_name(),
 +                'id' => $discussion->get_id(),
 +                'posts' => [
 +                    'userposts' => $postbuilder->build($user, [$forum], [$discussion], $posts),
 +                    'parentposts' => $parentposts,
 +                ],
 +            ];
 +        }
 +
 +        return [
 +                'discussions' => $builtdiscussions,
 +                'warnings' => $warnings,
 +        ];
 +    }
 +
 +    /**
 +     * Describe the post parameters.
 +     *
 +     * @return external_function_parameters
 +     */
 +    public static function get_discussion_posts_by_userid_parameters() {
 +        return new external_function_parameters ([
 +                'userid' => new external_value(
 +                        PARAM_INT, 'The ID of the user of whom to fetch posts.', VALUE_REQUIRED),
 +                'cmid' => new external_value(
 +                        PARAM_INT, 'The ID of the module of which to fetch items.', VALUE_REQUIRED),
 +                'sortby' => new external_value(
 +                        PARAM_ALPHA, 'Sort by this element: id, created or modified', VALUE_DEFAULT, 'created'),
 +                'sortdirection' => new external_value(
 +                        PARAM_ALPHA, 'Sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC')
 +        ]);
 +    }
 +
 +    /**
 +     * Describe the post return format.
 +     *
 +     * @return external_single_structure
 +     */
 +    public static function get_discussion_posts_by_userid_returns() {
 +        return new external_single_structure([
 +                'discussions' => new external_multiple_structure(
 +                    new external_single_structure([
 +                        'name' => new external_value(PARAM_RAW, 'Name of the discussion'),
 +                        'id' => new external_value(PARAM_INT, 'ID of the discussion'),
 +                        'posts' => new external_single_structure([
 +                            'userposts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
 +                            'parentposts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
 +                        ]),
 +                    ])),
 +                'warnings' => new external_warnings(),
 +        ]);
 +    }
++
+     /**
+      * Returns description of method parameters
+      *
+      * @return external_function_parameters
+      * @since Moodle 3.8
+      */
+     public static function get_discussion_post_parameters() {
+         return new external_function_parameters(
+             array(
+                 'postid' => new external_value(PARAM_INT, 'Post to fetch.'),
+             )
+         );
+     }
++
+     /**
+      * Get a particular discussion post.
+      *
+      * @param int $postid post to fetch
+      * @return array of post and warnings (if any)
+      * @since Moodle 3.8
+      * @throws moodle_exception
+      */
+     public static function get_discussion_post($postid) {
+         global $USER, $CFG;
+         $params = self::validate_parameters(self::get_discussion_post_parameters(),
+                                             array(
+                                                 'postid' => $postid,
+                                             ));
+         $warnings = array();
+         $vaultfactory = mod_forum\local\container::get_vault_factory();
+         $forumvault = $vaultfactory->get_forum_vault();
+         $discussionvault = $vaultfactory->get_discussion_vault();
+         $postvault = $vaultfactory->get_post_vault();
+         $postentity = $postvault->get_from_id($params['postid']);
+         if (empty($postentity)) {
+             throw new moodle_exception('invalidpostid', 'forum');
+         }
+         $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
+         if (empty($discussionentity)) {
+             throw new moodle_exception('notpartofdiscussion', 'forum');
+         }
+         $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
+         if (empty($forumentity)) {
+             throw new moodle_exception('invalidforumid', 'forum');
+         }
+         self::validate_context($forumentity->get_context());
+         $managerfactory = mod_forum\local\container::get_manager_factory();
+         $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
+         if (!$capabilitymanager->can_view_post($USER, $discussionentity, $postentity)) {
+             throw new moodle_exception('noviewdiscussionspermission', 'forum');
+         }
+         $builderfactory = mod_forum\local\container::get_builder_factory();
+         $postbuilder = $builderfactory->get_exported_posts_builder();
+         $posts = $postbuilder->build($USER, [$forumentity], [$discussionentity], [$postentity]);
+         $post = empty($posts) ? array() : reset($posts);
+         $result = array();
+         $result['post'] = $post;
+         $result['warnings'] = $warnings;
+         return $result;
+     }
++
+     /**
+      * Returns description of method result value
+      *
+      * @return external_description
+      * @since Moodle 3.8
+      */
+     public static function get_discussion_post_returns() {
+         return new external_single_structure(
+             array(
+                 'post' => \mod_forum\local\exporters\post::get_read_structure(),
+                 'warnings' => new external_warnings(),
+             )
+         );
+     }
+     /**
+      * Returns description of method parameters
+      *
+      * @return external_function_parameters
+      * @since Moodle 3.8
+      */
+     public static function prepare_draft_area_for_post_parameters() {
+         return new external_function_parameters(
+             array(
+                 'postid' => new external_value(PARAM_INT, 'Post to prepare the draft area for.'),
+                 'area' => new external_value(PARAM_ALPHA, 'Area to prepare: attachment or post.'),
+                 'draftitemid' => new external_value(PARAM_INT, 'The draft item id to use. 0 to generate one.',
+                     VALUE_DEFAULT, 0),
+                 'filestokeep' => new external_multiple_structure(
+                     new external_single_structure(
+                         array(
+                             'filename' => new external_value(PARAM_FILE, 'File name.'),
+                             'filepath' => new external_value(PARAM_PATH, 'File path.'),
+                         )
+                     ), 'Only keep these files in the draft file area. Empty for keeping all.', VALUE_DEFAULT, []
+                 ),
+             )
+         );
+     }
++
+     /**
+      * Prepares a draft area for editing a post.
+      *
+      * @param int $postid post to prepare the draft area for
+      * @param string $area area to prepare attachment or post
+      * @param int $draftitemid the draft item id to use. 0 to generate a new one.
+      * @param array $filestokeep only keep these files in the draft file area. Empty for keeping all.
+      * @return array of files in the area, the area options and the draft item id
+      * @since Moodle 3.8
+      * @throws moodle_exception
+      */
+     public static function prepare_draft_area_for_post($postid, $area, $draftitemid = 0, $filestokeep = []) {
+         global $USER;
+         $params = self::validate_parameters(
+             self::prepare_draft_area_for_post_parameters(),
+             array(
+                 'postid' => $postid,
+                 'area' => $area,
+                 'draftitemid' => $draftitemid,
+                 'filestokeep' => $filestokeep,
+             )
+         );
+         $directionallowedvalues = ['ASC', 'DESC'];
+         $allowedareas = ['attachment', 'post'];
+         if (!in_array($params['area'], $allowedareas)) {
+             throw new invalid_parameter_exception('Invalid value for area parameter
+                 (value: ' . $params['area'] . '),' . 'allowed values are: ' . implode(', ', $allowedareas));
+         }
+         $warnings = array();
+         $vaultfactory = mod_forum\local\container::get_vault_factory();
+         $forumvault = $vaultfactory->get_forum_vault();
+         $discussionvault = $vaultfactory->get_discussion_vault();
+         $postvault = $vaultfactory->get_post_vault();
+         $postentity = $postvault->get_from_id($params['postid']);
+         if (empty($postentity)) {
+             throw new moodle_exception('invalidpostid', 'forum');
+         }
+         $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
+         if (empty($discussionentity)) {
+             throw new moodle_exception('notpartofdiscussion', 'forum');
+         }
+         $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
+         if (empty($forumentity)) {
+             throw new moodle_exception('invalidforumid', 'forum');
+         }
+         $context = $forumentity->get_context();
+         self::validate_context($context);
+         $managerfactory = mod_forum\local\container::get_manager_factory();
+         $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
+         if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) {
+             throw new moodle_exception('noviewdiscussionspermission', 'forum');
+         }
+         if ($params['area'] == 'attachment') {
+             $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
+             $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
+             $forum = $forumdatamapper->to_legacy_object($forumentity);
+             $areaoptions = mod_forum_post_form::attachment_options($forum);
+             $messagetext = null;
+         } else {
+             $areaoptions = mod_forum_post_form::editor_options($context, $postentity->get_id());
+             $messagetext = $postentity->get_message();
+         }
+         $draftitemid = empty($params['draftitemid']) ? 0 : $params['draftitemid'];
+         $messagetext = file_prepare_draft_area($draftitemid, $context->id, 'mod_forum', $params['area'],
+             $postentity->get_id(), $areaoptions, $messagetext);
+         // Just get a structure compatible with external API.
+         array_walk($areaoptions, function(&$item, $key) {
+             $item = ['name' => $key, 'value' => $item];
+         });
+         // Do we need to keep only the given files?
+         $usercontext = context_user::instance($USER->id);
+         if (!empty($params['filestokeep'])) {
+             $fs = get_file_storage();
+             if ($areafiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid)) {
+                 $filestokeep = [];
+                 foreach ($params['filestokeep'] as $ftokeep) {
+                     $filestokeep[$ftokeep['filepath']][$ftokeep['filename']] = $ftokeep;
+                 }
+                 foreach ($areafiles as $file) {
+                     if ($file->is_directory()) {
+                         continue;
+                     }
+                     if (!isset($filestokeep[$file->get_filepath()][$file->get_filename()])) {
+                         $file->delete();    // Not in the list to be kept.
+                     }
+                 }
+             }
+         }
+         $result = array(
+             'draftitemid' => $draftitemid,
+             'files' => external_util::get_area_files($usercontext->id, 'user', 'draft',
+                 $draftitemid),
+             'areaoptions' => $areaoptions,
+             'messagetext' => $messagetext,
+             'warnings' => $warnings,
+         );
+         return $result;
+     }
++
+     /**
+      * Returns description of method result value
+      *
+      * @return external_description
+      * @since Moodle 3.8
+      */
+     public static function prepare_draft_area_for_post_returns() {
+         return new external_single_structure(
+             array(
+                 'draftitemid' => new external_value(PARAM_INT, 'Draft item id for the file area.'),
+                 'files' => new external_files('Draft area files.', VALUE_OPTIONAL),
+                 'areaoptions' => new external_multiple_structure(
+                     new external_single_structure(
+                         array(
+                             'name' => new external_value(PARAM_RAW, 'Name of option.'),
+                             'value' => new external_value(PARAM_RAW, 'Value of option.'),
+                         )
+                     ), 'Draft file area options.'
+                 ),
+                 'messagetext' => new external_value(PARAM_RAW, 'Message text with URLs rewritten.'),
+                 'warnings' => new external_warnings(),
+             )
+         );
+     }
+     /**
+      * Returns description of method parameters
+      *
+      * @return external_function_parameters
+      * @since Moodle 3.8
+      */
+     public static function update_discussion_post_parameters() {
+         return new external_function_parameters(
+             [
+                 'postid' => new external_value(PARAM_INT, 'Post to be updated. It can be a discussion topic post.'),
+                 'subject' => new external_value(PARAM_TEXT, 'Updated post subject', VALUE_DEFAULT, ''),
+                 'message' => new external_value(PARAM_RAW, 'Updated post message (HTML assumed if messageformat is not provided)',
+                     VALUE_DEFAULT, ''),
+                 'messageformat' => new external_format_value('message', VALUE_DEFAULT),
+                 'options' => new external_multiple_structure (
+                     new external_single_structure(
+                         [
+                             'name' => new external_value(
+                                 PARAM_ALPHANUM,
+                                 'The allowed keys (value format) are:
+                                 pinned (bool); (only for discussions) whether to pin this discussion or not
+                                 discussionsubscribe (bool); whether to subscribe to the post or not
+                                 inlineattachmentsid (int); the draft file area id for inline attachments in the text
+                                 attachmentsid (int); the draft file area id for attachments'
+                             ),
+                             'value' => new external_value(PARAM_RAW, 'The value of the option.')
+                         ]
+                     ),
+                     'Configuration options for the post.',
+                     VALUE_DEFAULT,
+                     []
+                 ),
+             ]
+         );
+     }
+     /**
+      * Updates a post or a discussion post topic.
+      *
+      * @param int $postid post to be updated, it can be a discussion topic post.
+      * @param string $subject updated post subject
+      * @param string $message updated post message (HTML assumed if messageformat is not provided)
+      * @param int $messageformat The format of the message, defaults to FORMAT_HTML
+      * @param array $options different configuration options for the post to be updated.
+      * @return array of warnings and the status (true if the post/discussion was deleted)
+      * @since Moodle 3.8
+      * @throws moodle_exception
+      * @todo support more options: timed posts, groups change and tags.
+      */
+     public static function update_discussion_post($postid, $subject = '', $message = '', $messageformat = FORMAT_HTML,
+             $options = []) {
+         global $CFG, $USER;
+         require_once($CFG->dirroot . "/mod/forum/lib.php");
+         $params = self::validate_parameters(self::add_discussion_post_parameters(),
+             [
+                 'postid' => $postid,
+                 'subject' => $subject,
+                 'message' => $message,
+                 'options' => $options,
+                 'messageformat' => $messageformat,
+             ]
+         );
+         $warnings = [];
+         // Validate options.
+         $options = [];
+         foreach ($params['options'] as $option) {
+             $name = trim($option['name']);
+             switch ($name) {
+                 case 'pinned':
+                     $value = clean_param($option['value'], PARAM_BOOL);
+                     break;
+                 case 'discussionsubscribe':
+                     $value = clean_param($option['value'], PARAM_BOOL);
+                     break;
+                 case 'inlineattachmentsid':
+                     $value = clean_param($option['value'], PARAM_INT);
+                     break;
+                 case 'attachmentsid':
+                     $value = clean_param($option['value'], PARAM_INT);
+                     break;
+                 default:
+                     throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
+             }
+             $options[$name] = $value;
+         }
+         $managerfactory = mod_forum\local\container::get_manager_factory();
+         $vaultfactory = mod_forum\local\container::get_vault_factory();
+         $forumvault = $vaultfactory->get_forum_vault();
+         $discussionvault = $vaultfactory->get_discussion_vault();
+         $postvault = $vaultfactory->get_post_vault();
+         $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
+         $forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
+         $discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper();
+         $postdatamapper = $legacydatamapperfactory->get_post_data_mapper();
+         $postentity = $postvault->get_from_id($params['postid']);
+         if (empty($postentity)) {
+             throw new moodle_exception('invalidpostid', 'forum');
+         }
+         $discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
+         if (empty($discussionentity)) {
+             throw new moodle_exception('notpartofdiscussion', 'forum');
+         }
+         $forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
+         if (empty($forumentity)) {
+             throw new moodle_exception('invalidforumid', 'forum');
+         }
+         $forum = $forumdatamapper->to_legacy_object($forumentity);
+         $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
+         $modcontext = $forumentity->get_context();
+         self::validate_context($modcontext);
+         if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) {
+             throw new moodle_exception('cannotupdatepost', 'forum');
+         }
+         // Get the original post.
+         $updatepost = $postdatamapper->to_legacy_object($postentity);
+         $updatepost->itemid = IGNORE_FILE_MERGE;
+         $updatepost->attachments = IGNORE_FILE_MERGE;
+         // Prepare the post to be updated.
+         if (!empty($params['subject'])) {
+             $updatepost->subject = $params['subject'];
+         }
+         if (!empty($params['message']) && !empty($params['messageformat'])) {
+             $updatepost->message       = $params['message'];
+             $updatepost->messageformat = $params['messageformat'];
+             $updatepost->messagetrust  = trusttext_trusted($modcontext);
+             // Clean message text.
+             $updatepost = trusttext_pre_edit($updatepost, 'message', $modcontext);
+         }
+         if (isset($options['discussionsubscribe'])) {
+             // No need to validate anything here, forum_post_subscription will do.
+             $updatepost->discussionsubscribe = $options['discussionsubscribe'];
+         }
+         // When editing first post/discussion.
+         if (!$postentity->has_parent()) {
+             // Defaults for discussion topic posts.
+             $updatepost->name = $discussionentity->get_name();
+             $updatepost->timestart = $discussionentity->get_time_start();
+             $updatepost->timeend = $discussionentity->get_time_end();
+             if (isset($options['pinned'])) {
+                 if ($capabilitymanager->can_pin_discussions($USER)) {
+                     // Can change pinned if we have capability.
+                     $updatepost->pinned = !empty($options['pinned']) ? FORUM_DISCUSSION_PINNED : FORUM_DISCUSSION_UNPINNED;
+                 }
+             }
+         }
+         if (isset($options['inlineattachmentsid'])) {
+             $updatepost->itemid = $options['inlineattachmentsid'];
+         }
+         if (isset($options['attachmentsid']) && forum_can_create_attachment($forum, $modcontext)) {
+             $updatepost->attachments = $options['attachmentsid'];
+         }
+         // Update the post.
+         $fakemform = $updatepost->id;
+         if (forum_update_post($updatepost, $fakemform)) {
+             $discussion = $discussiondatamapper->to_legacy_object($discussionentity);
+             forum_trigger_post_updated_event($updatepost, $discussion, $modcontext, $forum);
+             forum_post_subscription(
+                 $updatepost,
+                 $forum,
+                 $discussion
+             );
+             $status = true;
+         } else {
+             $status = false;
+         }
+         return [
+             'status' => $status,
+             'warnings' => $warnings,
+         ];
+     }
+     /**
+      * Returns description of method result value
+      *
+      * @return external_description
+      * @since Moodle 3.8
+      */
+     public static function update_discussion_post_returns() {
+         return new external_single_structure(
+             [
+                 'status' => new external_value(PARAM_BOOL, 'True if the post/discussion was updated, false otherwise.'),
+                 'warnings' => new external_warnings()
+             ]
+         );
+     }
  }
Simple merge
@@@ -2605,396 -2605,346 +2605,739 @@@ class mod_forum_external_testcase exten
          mod_forum_external::delete_post($post->id);
      }
  
 +    /*
 +     * Test get forum posts by user id.
 +     */
 +    public function test_mod_forum_get_discussion_posts_by_userid() {
 +        $this->resetAfterTest(true);
 +
 +        $urlfactory = mod_forum\local\container::get_url_factory();
 +        $entityfactory = mod_forum\local\container::get_entity_factory();
 +        $vaultfactory = mod_forum\local\container::get_vault_factory();
 +        $postvault = $vaultfactory->get_post_vault();
 +        $legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
 +        $legacypostmapper = $legacydatamapper->get_post_data_mapper();
 +
 +        $user1 = self::getDataGenerator()->create_user();
 +        $user1entity = $entityfactory->get_author_from_stdclass($user1);
 +        $exporteduser1 = [
 +            'id' => (int) $user1->id,
 +            'fullname' => fullname($user1),
 +            'groups' => [],
 +            'urls' => [
 +                'profile' => $urlfactory->get_author_profile_url($user1entity),
 +                'profileimage' => $urlfactory->get_author_profile_image_url($user1entity),
 +            ],
 +            'isdeleted' => false,
 +        ];
 +        // Create a bunch of other users to post.
 +        $user2 = self::getDataGenerator()->create_user();
 +        $user2entity = $entityfactory->get_author_from_stdclass($user2);
 +        $exporteduser2 = [
 +            'id' => (int) $user2->id,
 +            'fullname' => fullname($user2),
 +            'groups' => [],
 +            'urls' => [
 +                'profile' => $urlfactory->get_author_profile_url($user2entity),
 +                'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
 +            ],
 +            'isdeleted' => false,
 +        ];
 +        $user2->fullname = $exporteduser2['fullname'];
 +
 +        $forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
 +
 +        // Set the first created user to the test user.
 +        self::setUser($user1);
 +
 +        // Create course to add the module.
 +        $course1 = self::getDataGenerator()->create_course();
 +
 +        // Forum with tracking off.
 +        $record = new stdClass();
 +        $record->course = $course1->id;
 +        $forum1 = self::getDataGenerator()->create_module('forum', $record);
 +        $forum1context = context_module::instance($forum1->cmid);
 +
 +        // Add discussions to the forums.
 +        $record = new stdClass();
 +        $record->course = $course1->id;
 +        $record->userid = $user1->id;
 +        $record->forum = $forum1->id;
 +        $discussion1 = $forumgenerator->create_discussion($record);
 +        $discussion1firstpost = $postvault->get_first_post_for_discussion_ids([$discussion1->id]);
 +        $discussion1firstpostobject = $legacypostmapper->to_legacy_object($discussion1firstpost[$discussion1->firstpost]);
 +
 +        $record = new stdClass();
 +        $record->course = $course1->id;
 +        $record->userid = $user1->id;
 +        $record->forum = $forum1->id;
 +        $discussion2 = $forumgenerator->create_discussion($record);
 +        $discussion2firstpost = $postvault->get_first_post_for_discussion_ids([$discussion2->id]);
 +        $discussion2firstpostobject = $legacypostmapper->to_legacy_object($discussion2firstpost[$discussion2->firstpost]);
 +
 +        // Add 1 reply to the discussion 1 from a different user.
 +        $record = new stdClass();
 +        $record->discussion = $discussion1->id;
 +        $record->parent = $discussion1->firstpost;
 +        $record->userid = $user2->id;
 +        $discussion1reply1 = $forumgenerator->create_post($record);
 +        $filename = 'shouldbeanimage.jpg';
 +        // Add a fake inline image to the post.
 +        $filerecordinline = array(
 +                'contextid' => $forum1context->id,
 +                'component' => 'mod_forum',
 +                'filearea'  => 'post',
 +                'itemid'    => $discussion1reply1->id,
 +                'filepath'  => '/',
 +                'filename'  => $filename,
 +        );
 +        $fs = get_file_storage();
 +        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 +
 +        // Add 1 reply to the discussion 2 from a different user.
 +        $record = new stdClass();
 +        $record->discussion = $discussion2->id;
 +        $record->parent = $discussion2->firstpost;
 +        $record->userid = $user2->id;
 +        $discussion2reply1 = $forumgenerator->create_post($record);
 +        $filename = 'shouldbeanimage.jpg';
 +        // Add a fake inline image to the post.
 +        $filerecordinline = array(
 +                'contextid' => $forum1context->id,
 +                'component' => 'mod_forum',
 +                'filearea'  => 'post',
 +                'itemid'    => $discussion2reply1->id,
 +                'filepath'  => '/',
 +                'filename'  => $filename,
 +        );
 +        $fs = get_file_storage();
 +        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
 +
 +        // Following line enrol and assign default role id to the user.
 +        // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
 +        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
 +        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
 +
 +        // Create what we expect to be returned when querying the discussion.
 +        $expectedposts = array(
 +            'discussions' => array(),
 +            'warnings' => array(),
 +        );
 +
 +        $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
 +        $isolatedurluser->params(['parent' => $discussion1reply1->id]);
 +        $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1firstpostobject->discussion);
 +        $isolatedurlparent->params(['parent' => $discussion1firstpostobject->id]);
 +
 +        $expectedposts['discussions'][0] = [
 +            'name' => $discussion1->name,
 +            'id' => $discussion1->id,
 +            'posts' => [
 +                'userposts' => [
 +                    [
 +                        'id' => $discussion1reply1->id,
 +                        'discussionid' => $discussion1reply1->discussion,
 +                        'parentid' => $discussion1reply1->parent,
 +                        'hasparent' => true,
 +                        'timecreated' => $discussion1reply1->created,
 +                        'subject' => $discussion1reply1->subject,
 +                        'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
 +                        'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
 +                        $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
 +                        'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 +                        'unread' => null,
 +                        'isdeleted' => false,
 +                        'isprivatereply' => false,
 +                        'haswordcount' => false,
 +                        'wordcount' => null,
 +                        'author' => $exporteduser2,
 +                        'attachments' => [],
 +                        'tags' => [],
 +                        'html' => [
 +                            'rating' => null,
 +                            'taglist' => null,
 +                            'authorsubheading' => $forumgenerator->get_author_subheading_html(
 +                                (object)$exporteduser2, $discussion1reply1->created)
 +                        ],
 +                        'charcount' => null,
 +                        'capabilities' => [
 +                            'view' => true,
 +                            'edit' => true,
 +                            'delete' => true,
 +                            'split' => false,
 +                            'reply' => true,
 +                            'export' => false,
 +                            'controlreadstatus' => false,
 +                            'canreplyprivately' => false,
 +                            'selfenrol' => false
 +                        ],
 +                        'urls' => [
 +                            'view' => $urlfactory->get_view_post_url_from_post_id(
 +                                    $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
 +                            'viewisolated' => $isolatedurluser->out(false),
 +                            'viewparent' => $urlfactory->get_view_post_url_from_post_id(
 +                                    $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
 +                            'edit' => (new moodle_url('/mod/forum/post.php', [
 +                                    'edit' => $discussion1reply1->id
 +                            ]))->out(false),
 +                            'delete' => (new moodle_url('/mod/forum/post.php', [
 +                                    'delete' => $discussion1reply1->id
 +                            ]))->out(false),
 +                            'split' => null,
 +                            'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 +                                    'reply' => $discussion1reply1->id
 +                            ]))->out(false),
 +                            'export' => null,
 +                            'markasread' => null,
 +                            'markasunread' => null,
 +                            'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
 +                                    $discussion1reply1->discussion)->out(false),
 +                        ],
 +                    ]
 +                ],
 +                'parentposts' => [
 +                    [
 +                        'id' => $discussion1firstpostobject->id,
 +                        'discussionid' => $discussion1firstpostobject->discussion,
 +                        'parentid' => null,
 +                        'hasparent' => false,
 +                        'timecreated' => $discussion1firstpostobject->created,
 +                        'subject' => $discussion1firstpostobject->subject,
 +                        'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
 +                        'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
 +                            $forum1context->id, 'mod_forum', 'post', $discussion1firstpostobject->id),
 +                        'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 +                        'unread' => null,
 +                        'isdeleted' => false,
 +                        'isprivatereply' => false,
 +                        'haswordcount' => false,
 +                        'wordcount' => null,
 +                        'author' => $exporteduser1,
 +                        'attachments' => [],
 +                        'tags' => [],
 +                        'html' => [
 +                            'rating' => null,
 +                            'taglist' => null,
 +                            'authorsubheading' => $forumgenerator->get_author_subheading_html(
 +                                (object)$exporteduser1, $discussion1firstpostobject->created)
 +                        ],
 +                        'charcount' => null,
 +                        'capabilities' => [
 +                            'view' => true,
 +                            'edit' => false,
 +                            'delete' => false,
 +                            'split' => false,
 +                            'reply' => true,
 +                            'export' => false,
 +                            'controlreadstatus' => false,
 +                            'canreplyprivately' => false,
 +                            'selfenrol' => false
 +                        ],
 +                        'urls' => [
 +                            'view' => $urlfactory->get_view_post_url_from_post_id(
 +                                $discussion1firstpostobject->discussion, $discussion1firstpostobject->id)->out(false),
 +                            'viewisolated' => $isolatedurlparent->out(false),
 +                            'viewparent' => null,
 +                            'edit' => null,
 +                            'delete' => null,
 +                            'split' => null,
 +                            'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 +                                'reply' => $discussion1firstpostobject->id
 +                            ]))->out(false),
 +                            'export' => null,
 +                            'markasread' => null,
 +                            'markasunread' => null,
 +                            'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
 +                                $discussion1firstpostobject->discussion)->out(false),
 +                        ],
 +                    ]
 +                ],
 +            ],
 +        ];
 +
 +        $isolatedurluser = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2reply1->discussion);
 +        $isolatedurluser->params(['parent' => $discussion2reply1->id]);
 +        $isolatedurlparent = $urlfactory->get_discussion_view_url_from_discussion_id($discussion2firstpostobject->discussion);
 +        $isolatedurlparent->params(['parent' => $discussion2firstpostobject->id]);
 +
 +        $expectedposts['discussions'][1] = [
 +            'name' => $discussion2->name,
 +            'id' => $discussion2->id,
 +            'posts' => [
 +                'userposts' => [
 +                    [
 +                        'id' => $discussion2reply1->id,
 +                        'discussionid' => $discussion2reply1->discussion,
 +                        'parentid' => $discussion2reply1->parent,
 +                        'hasparent' => true,
 +                        'timecreated' => $discussion2reply1->created,
 +                        'subject' => $discussion2reply1->subject,
 +                        'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
 +                        'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
 +                            $forum1context->id, 'mod_forum', 'post', $discussion2reply1->id),
 +                        'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 +                        'unread' => null,
 +                        'isdeleted' => false,
 +                        'isprivatereply' => false,
 +                        'haswordcount' => false,
 +                        'wordcount' => null,
 +                        'author' => $exporteduser2,
 +                        'attachments' => [],
 +                        'tags' => [],
 +                        'html' => [
 +                            'rating' => null,
 +                            'taglist' => null,
 +                            'authorsubheading' => $forumgenerator->get_author_subheading_html(
 +                                (object)$exporteduser2, $discussion2reply1->created)
 +                        ],
 +                        'charcount' => null,
 +                        'capabilities' => [
 +                            'view' => true,
 +                            'edit' => true,
 +                            'delete' => true,
 +                            'split' => false,
 +                            'reply' => true,
 +                            'export' => false,
 +                            'controlreadstatus' => false,
 +                            'canreplyprivately' => false,
 +                            'selfenrol' => false
 +                        ],
 +                        'urls' => [
 +                            'view' => $urlfactory->get_view_post_url_from_post_id(
 +                                $discussion2reply1->discussion, $discussion2reply1->id)->out(false),
 +                            'viewisolated' => $isolatedurluser->out(false),
 +                            'viewparent' => $urlfactory->get_view_post_url_from_post_id(
 +                                $discussion2reply1->discussion, $discussion2reply1->parent)->out(false),
 +                            'edit' => (new moodle_url('/mod/forum/post.php', [
 +                                'edit' => $discussion2reply1->id
 +                            ]))->out(false),
 +                            'delete' => (new moodle_url('/mod/forum/post.php', [
 +                                'delete' => $discussion2reply1->id
 +                            ]))->out(false),
 +                            'split' => null,
 +                            'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 +                                'reply' => $discussion2reply1->id
 +                            ]))->out(false),
 +                            'export' => null,
 +                            'markasread' => null,
 +                            'markasunread' => null,
 +                            'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
 +                                $discussion2reply1->discussion)->out(false),
 +                        ],
 +                    ]
 +                ],
 +                'parentposts' => [
 +                    [
 +                        'id' => $discussion2firstpostobject->id,
 +                        'discussionid' => $discussion2firstpostobject->discussion,
 +                        'parentid' => null,
 +                        'hasparent' => false,
 +                        'timecreated' => $discussion2firstpostobject->created,
 +                        'subject' => $discussion2firstpostobject->subject,
 +                        'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
 +                        'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
 +                            $forum1context->id, 'mod_forum', 'post', $discussion2firstpostobject->id),
 +                        'messageformat' => 1,   // This value is usually changed by external_format_text() function.
 +                        'unread' => null,
 +                        'isdeleted' => false,
 +                        'isprivatereply' => false,
 +                        'haswordcount' => false,
 +                        'wordcount' => null,
 +                        'author' => $exporteduser1,
 +                        'attachments' => [],
 +                        'tags' => [],
 +                        'html' => [
 +                            'rating' => null,
 +                            'taglist' => null,
 +                            'authorsubheading' => $forumgenerator->get_author_subheading_html(
 +                                (object)$exporteduser1, $discussion2firstpostobject->created)
 +                        ],
 +                        'charcount' => null,
 +                        'capabilities' => [
 +                            'view' => true,
 +                            'edit' => false,
 +                            'delete' => false,
 +                            'split' => false,
 +                            'reply' => true,
 +                            'export' => false,
 +                            'controlreadstatus' => false,
 +                            'canreplyprivately' => false,
 +                            'selfenrol' => false
 +                        ],
 +                        'urls' => [
 +                            'view' => $urlfactory->get_view_post_url_from_post_id(
 +                                $discussion2firstpostobject->discussion, $discussion2firstpostobject->id)->out(false),
 +                            'viewisolated' => $isolatedurlparent->out(false),
 +                            'viewparent' => null,
 +                            'edit' => null,
 +                            'delete' => null,
 +                            'split' => null,
 +                            'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
 +                                'reply' => $discussion2firstpostobject->id
 +                            ]))->out(false),
 +                            'export' => null,
 +                            'markasread' => null,
 +                            'markasunread' => null,
 +                            'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
 +                                $discussion2firstpostobject->discussion)->out(false),
 +
 +                        ]
 +                    ],
 +                ]
 +            ],
 +        ];
 +
 +        // Test discussions with one additional post each (total 2 posts).
 +        // Also testing that we get the parent posts too.
 +        $discussions = mod_forum_external::get_discussion_posts_by_userid($user2->id, $forum1->cmid, 'modified', 'DESC');
 +        $discussions = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_by_userid_returns(), $discussions);
 +
 +        $this->assertEquals(2, count($discussions['discussions']));
 +
 +        $this->assertEquals($expectedposts, $discussions);
 +    }
++
+     /**
+      * Test get_discussion_post a discussion.
+      */
+     public function test_get_discussion_post_discussion() {
+         global $DB;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $user = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         // Add a discussion.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         $this->setUser($user);
+         $result = mod_forum_external::get_discussion_post($discussion->firstpost);
+         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
+         $this->assertEquals($discussion->firstpost, $result['post']['id']);
+         $this->assertFalse($result['post']['hasparent']);
+         $this->assertEquals($discussion->message, $result['post']['message']);
+     }
+     /**
+      * Test get_discussion_post a post.
+      */
+     public function test_get_discussion_post_post() {
+         global $DB;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $user = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         // Add a discussion.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
+         // Add a post.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $record->discussion = $discussion->id;
+         $record->parent = $parentpost->id;
+         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+         $this->setUser($user);
+         $result = mod_forum_external::get_discussion_post($post->id);
+         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
+         $this->assertEquals($post->id, $result['post']['id']);
+         $this->assertTrue($result['post']['hasparent']);
+         $this->assertEquals($post->message, $result['post']['message']);
+     }
+     /**
+      * Test get_discussion_post a different user post.
+      */
+     public function test_get_discussion_post_other_user_post() {
+         global $DB;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $user = $this->getDataGenerator()->create_user();
+         $otheruser = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         self::getDataGenerator()->enrol_user($otheruser->id, $course->id, $role->id);
+         // Add a discussion.
+         $record = array();
+         $record['course'] = $course->id;
+         $record['forum'] = $forum->id;
+         $record['userid'] = $user->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
+         // Add a post.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $record->discussion = $discussion->id;
+         $record->parent = $parentpost->id;
+         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+         // Check other user post.
+         $this->setUser($otheruser);
+         $result = mod_forum_external::get_discussion_post($post->id);
+         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
+         $this->assertEquals($post->id, $result['post']['id']);
+         $this->assertTrue($result['post']['hasparent']);
+         $this->assertEquals($post->message, $result['post']['message']);
+     }
+     /**
+      * Test prepare_draft_area_for_post a different user post.
+      */
+     public function test_prepare_draft_area_for_post() {
+         global $DB;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $user = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         // Add a discussion.
+         $record = array();
+         $record['course'] = $course->id;
+         $record['forum'] = $forum->id;
+         $record['userid'] = $user->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         $parentpost = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
+         // Add a post.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $record->discussion = $discussion->id;
+         $record->parent = $parentpost->id;
+         $post = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+         // Add some files only in the attachment area.
+         $filename = 'faketxt.txt';
+         $filerecordinline = array(
+             'contextid' => context_module::instance($forum->cmid)->id,
+             'component' => 'mod_forum',
+             'filearea'  => 'attachment',
+             'itemid'    => $post->id,
+             'filepath'  => '/',
+             'filename'  => $filename,
+         );
+         $fs = get_file_storage();
+         $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
+         $filerecordinline['filename'] = 'otherfaketxt.txt';
+         $fs->create_file_from_string($filerecordinline, 'fake txt contents 2.');
+         $this->setUser($user);
+         // Check attachment area.
+         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment');
+         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
+         $this->assertCount(2, $result['files']);
+         $this->assertEquals($filename, $result['files'][0]['filename']);
+         $this->assertCount(5, $result['areaoptions']);
+         $this->assertEmpty($result['messagetext']);
+         // Check again using existing draft item id.
+         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid']);
+         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
+         $this->assertCount(2, $result['files']);
+         // Keep only certain files in the area.
+         $filestokeep = array(array('filename' => $filename, 'filepath' => '/'));
+         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'attachment', $result['draftitemid'], $filestokeep);
+         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
+         $this->assertCount(1, $result['files']);
+         $this->assertEquals($filename, $result['files'][0]['filename']);
+         // Check editor (post) area.
+         $filerecordinline['filearea'] = 'post';
+         $filerecordinline['filename'] = 'fakeimage.png';
+         $fs->create_file_from_string($filerecordinline, 'fake image.');
+         $result = mod_forum_external::prepare_draft_area_for_post($post->id, 'post');
+         $result = external_api::clean_returnvalue(mod_forum_external::prepare_draft_area_for_post_returns(), $result);
+         $this->assertCount(1, $result['files']);
+         $this->assertEquals($filerecordinline['filename'], $result['files'][0]['filename']);
+         $this->assertCount(5, $result['areaoptions']);
+         $this->assertEquals($post->message, $result['messagetext']);
+     }
+     /**
+      * Test update_discussion_post with a discussion.
+      */
+     public function test_update_discussion_post_discussion() {
+         global $DB, $USER;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $this->setAdminUser();
+         // Add a discussion.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $USER->id;
+         $record->forum = $forum->id;
+         $record->pinned = FORUM_DISCUSSION_UNPINNED;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         $subject = 'Hey subject updated';
+         $message = 'Hey message updated';
+         $messageformat = FORMAT_HTML;
+         $options = [
+             ['name' => 'pinned', 'value' => true],
+         ];
+         $result = mod_forum_external::update_discussion_post($discussion->firstpost, $subject, $message, $messageformat,
+             $options);
+         $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
+         $this->assertTrue($result['status']);
+         // Get the post from WS.
+         $result = mod_forum_external::get_discussion_post($discussion->firstpost);
+         $result = external_api::clean_returnvalue(mod_forum_external::get_discussion_post_returns(), $result);
+         $this->assertEquals($subject, $result['post']['subject']);
+         $this->assertEquals($message, $result['post']['message']);
+         $this->assertEquals($messageformat, $result['post']['messageformat']);
+         // Get discussion object from DB.
+         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
+         $this->assertEquals($subject, $discussion->name);   // Check discussion subject.
+         $this->assertEquals(FORUM_DISCUSSION_PINNED, $discussion->pinned);  // Check discussion pinned.
+     }
+     /**
+      * Test update_discussion_post with a post.
+      */
+     public function test_update_discussion_post_post() {
+         global $DB, $USER;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
+         $user = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         $this->setUser($user);
+         // Enable auto subscribe discussion.
+         $USER->autosubscribe = true;
+         // Add a discussion.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $user->id;
+         $record->forum = $forum->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         // Add a post via WS (so discussion subscription works).
+         $result = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
+         $newpost = $result['post'];
+         $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
+         // Add files in the different areas.
+         $draftidattach = file_get_unused_draft_itemid();
+         $filerecordinline = array(
+             'contextid' => context_user::instance($user->id)->id,
+             'component' => 'user',
+             'filearea'  => 'draft',
+             'itemid'    => $draftidattach,
+             'filepath'  => '/',
+             'filename'  => 'faketxt.txt',
+         );
+         $fs = get_file_storage();
+         $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
+         // Create files in post area (inline).
+         $draftidinlineattach = file_get_unused_draft_itemid();
+         $filerecordinline['itemid'] = $draftidinlineattach;
+         $filerecordinline['filename'] = 'fakeimage.png';
+         $fs->create_file_from_string($filerecordinline, 'img...');
+         // Do not update subject.
+         $message = 'Hey message updated';
+         $messageformat = FORMAT_HTML;
+         $options = [
+             ['name' => 'discussionsubscribe', 'value' => false],
+             ['name' => 'inlineattachmentsid', 'value' => $draftidinlineattach],
+             ['name' => 'attachmentsid', 'value' => $draftidattach],
+         ];
+         $result = mod_forum_external::update_discussion_post($newpost->id, '', $message, $messageformat, $options);
+         $result = external_api::clean_returnvalue(mod_forum_external::update_discussion_post_returns(), $result);
+         $this->assertTrue($result['status']);
+         // Check subscription status.
+         $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion->id, $cm));
+         // Get the post from WS.
+         $result = mod_forum_external::get_forum_discussion_posts($discussion->id);
+         $result = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $result);
+         $found = false;
+         foreach ($result['posts'] as $post) {
+             if ($post['id'] == $newpost->id) {
+                 $this->assertEquals($newpost->subject, $post['subject']);
+                 $this->assertEquals($message, $post['message']);
+                 $this->assertEquals($messageformat, $post['messageformat']);
+                 $this->assertCount(1, $post['messageinlinefiles']);
+                 $this->assertEquals('fakeimage.png', $post['messageinlinefiles'][0]['filename']);
+                 $this->assertCount(1, $post['attachments']);
+                 $this->assertEquals('faketxt.txt', $post['attachments'][0]['filename']);
+                 $found = true;
+             }
+         }
+         $this->assertTrue($found);
+     }
+     /**
+      * Test update_discussion_post with other user post (no permissions).
+      */
+     public function test_update_discussion_post_other_user_post() {
+         global $DB, $USER;
+         $this->resetAfterTest(true);
+         // Setup test data.
+         $course = $this->getDataGenerator()->create_course();
+         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+         $user = $this->getDataGenerator()->create_user();
+         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+         self::getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+         $this->setAdminUser();
+         // Add a discussion.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $USER->id;
+         $record->forum = $forum->id;
+         $discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+         // Add a post.
+         $record = new stdClass();
+         $record->course = $course->id;
+         $record->userid = $USER->id;
+         $record->forum = $forum->id;
+         $record->discussion = $discussion->id;
+         $record->parent = $discussion->firstpost;
+         $newpost = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+         $this->setUser($user);
+         $subject = 'Hey subject updated';
+         $message = 'Hey message updated';
+         $messageformat = FORMAT_HTML;
+         $this->expectExceptionMessage(get_string('cannotupdatepost', 'forum'));
+         mod_forum_external::update_discussion_post($newpost->id, $subject, $message, $messageformat);
+     }
  }
@@@ -24,6 -24,6 +24,6 @@@
  
  defined('MOODLE_INTERNAL') || die();
  
- $plugin->version   = 2019100105;       // The current module version (Date: YYYYMMDDXX)
 -$plugin->version   = 2019071904;       // The current module version (Date: YYYYMMDDXX)
++$plugin->version   = 2019100106;       // The current module version (Date: YYYYMMDDXX)
  $plugin->requires  = 2019051100;       // Requires this Moodle version
  $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)