See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Exported post builder class. 19 * 20 * @package mod_forum 21 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace mod_forum\local\builders; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use mod_forum\local\entities\discussion as discussion_entity; 30 use mod_forum\local\entities\forum as forum_entity; 31 use mod_forum\local\entities\post as post_entity; 32 use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; 33 use mod_forum\local\factories\exporter as exporter_factory; 34 use mod_forum\local\factories\vault as vault_factory; 35 use context; 36 use core_tag_tag; 37 use moodle_exception; 38 use rating_manager; 39 use renderer_base; 40 use stdClass; 41 42 /** 43 * Exported post builder class. 44 * 45 * This class is an implementation of the builder pattern (loosely). It is responsible 46 * for taking a set of related forums, discussions, and posts and generate the exported 47 * version of the posts. 48 * 49 * It encapsulates the complexity involved with exporting posts. All of the relevant 50 * additional resources will be loaded by this class in order to ensure the exporting 51 * process can happen. 52 * 53 * See this doc for more information on the builder pattern: 54 * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html 55 * 56 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 57 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 58 */ 59 class exported_posts { 60 /** @var renderer_base $renderer Core renderer */ 61 private $renderer; 62 63 /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */ 64 private $legacydatamapperfactory; 65 66 /** @var exporter_factory $exporterfactory Exporter factory */ 67 private $exporterfactory; 68 69 /** @var vault_factory $vaultfactory Vault factory */ 70 private $vaultfactory; 71 72 /** @var rating_manager $ratingmanager Rating manager */ 73 private $ratingmanager; 74 75 /** 76 * Constructor. 77 * 78 * @param renderer_base $renderer Core renderer 79 * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory 80 * @param exporter_factory $exporterfactory Exporter factory 81 * @param vault_factory $vaultfactory Vault factory 82 * @param rating_manager $ratingmanager Rating manager 83 */ 84 public function __construct( 85 renderer_base $renderer, 86 legacy_data_mapper_factory $legacydatamapperfactory, 87 exporter_factory $exporterfactory, 88 vault_factory $vaultfactory, 89 rating_manager $ratingmanager 90 ) { 91 $this->renderer = $renderer; 92 $this->legacydatamapperfactory = $legacydatamapperfactory; 93 $this->exporterfactory = $exporterfactory; 94 $this->vaultfactory = $vaultfactory; 95 $this->ratingmanager = $ratingmanager; 96 } 97 98 /** 99 * Build the exported posts for a given set of forums, discussions, and posts. 100 * 101 * This will typically be used for a list of posts in the same discussion/forum however 102 * it does support exporting any arbitrary list of posts as long as the caller also provides 103 * a unique list of all discussions for the list of posts and all forums for the list of discussions. 104 * 105 * Increasing the number of different forums being processed will increase the processing time 106 * due to processing multiple contexts (for things like capabilities, files, etc). The code attempts 107 * to load the additional resources as efficiently as possible but there is no way around some of 108 * the additional overhead. 109 * 110 * Note: Some posts will be removed as part of the build process according to capabilities. 111 * A one-to-one mapping should not be expected. 112 * 113 * @param stdClass $user The user to export the posts for. 114 * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to 115 * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to 116 * @param post_entity[] $posts The list of posts to export. 117 * @return stdClass[] List of exported posts in the same order as the $posts array. 118 */ 119 public function build( 120 stdClass $user, 121 array $forums, 122 array $discussions, 123 array $posts 124 ) : array { 125 // Format the forums and discussion to make them more easily accessed later. 126 $forums = array_reduce($forums, function($carry, $forum) { 127 $carry[$forum->get_id()] = $forum; 128 return $carry; 129 }, []); 130 $discussions = array_reduce($discussions, function($carry, $discussion) { 131 $carry[$discussion->get_id()] = $discussion; 132 return $carry; 133 }, []); 134 135 // Group the posts by discussion and forum so that we can load the resources in 136 // batches to improve performance. 137 $groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts); 138 // Load all of the resources we need in order to export the posts. 139 $authorsbyid = $this->get_authors_for_posts($posts); 140 $authorcontextids = $this->get_author_context_ids(array_keys($authorsbyid)); 141 $attachmentsbypostid = $this->get_attachments_for_posts($groupedposts); 142 $groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts); 143 $tagsbypostid = $this->get_tags_from_posts($posts); 144 $ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts); 145 $readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts); 146 $exportedposts = []; 147 148 // Export each set of posts per discussion because it's the largest chunks we can 149 // break them into due to constraints on capability checks. 150 foreach ($groupedposts as $grouping) { 151 [ 152 'forum' => $forum, 153 'discussion' => $discussion, 154 'posts' => $groupedposts 155 ] = $grouping; 156 157 $forumid = $forum->get_id(); 158 $courseid = $forum->get_course_record()->id; 159 $postsexporter = $this->exporterfactory->get_posts_exporter( 160 $user, 161 $forum, 162 $discussion, 163 $groupedposts, 164 $authorsbyid, 165 $authorcontextids, 166 $attachmentsbypostid, 167 $groupsbycourseandauthorid[$courseid], 168 $readreceiptcollectionbyforumid[$forumid] ?? null, 169 $tagsbypostid, 170 $ratingbypostid, 171 true 172 ); 173 ['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer); 174 $exportedposts = array_merge($exportedposts, $exportedgroupedposts); 175 } 176 177 if (count($forums) == 1 && count($discussions) == 1) { 178 // All of the posts belong to a single discussion in a single forum so 179 // the exported order will match the given $posts array. 180 return $exportedposts; 181 } else { 182 // Since we grouped the posts by discussion and forum the ordering of the 183 // exported posts may be different to the given $posts array so we should 184 // sort it back into the correct order for the caller. 185 return $this->sort_exported_posts($posts, $exportedposts); 186 } 187 } 188 189 /** 190 * Group the posts by which discussion they belong to in order for them to be processed 191 * in chunks by the exporting. 192 * 193 * Returns a list of groups where each group has a forum, discussion, and list of posts. 194 * E.g. 195 * [ 196 * [ 197 * 'forum' => <forum_entity>, 198 * 'discussion' => <discussion_entity>, 199 * 'posts' => [ 200 * <post_entity in discussion>, 201 * <post_entity in discussion>, 202 * <post_entity in discussion> 203 * ] 204 * ] 205 * ] 206 * 207 * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id. 208 * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id. 209 * @param post_entity[] $posts The list of posts to process. 210 * @return array List of grouped posts. Each group has a discussion, forum, and posts. 211 */ 212 private function group_posts_by_discussion(array $forums, array $discussions, array $posts) : array { 213 return array_reduce($posts, function($carry, $post) use ($forums, $discussions) { 214 $discussionid = $post->get_discussion_id(); 215 if (!isset($discussions[$discussionid])) { 216 throw new moodle_exception('Unable to find discussion with id ' . $discussionid); 217 } 218 219 if (isset($carry[$discussionid])) { 220 $carry[$discussionid]['posts'][] = $post; 221 } else { 222 $discussion = $discussions[$discussionid]; 223 $forumid = $discussion->get_forum_id(); 224 225 if (!isset($forums[$forumid])) { 226 throw new moodle_exception('Unable to find forum with id ' . $forumid); 227 } 228 229 $carry[$discussionid] = [ 230 'forum' => $forums[$forumid], 231 'discussion' => $discussions[$discussionid], 232 'posts' => [$post] 233 ]; 234 } 235 236 return $carry; 237 }, []); 238 } 239 240 /** 241 * Load the list of authors for the given posts. 242 * 243 * The list of authors will be indexed by the author id. 244 * 245 * @param post_entity[] $posts The list of posts to process. 246 * @return author_entity[] 247 */ 248 private function get_authors_for_posts(array $posts) : array { 249 $authorvault = $this->vaultfactory->get_author_vault(); 250 return $authorvault->get_authors_for_posts($posts); 251 } 252 253 /** 254 * Get the user context ids for each of the authors. 255 * 256 * @param int[] $authorids The list of author ids to fetch context ids for. 257 * @return int[] Context ids indexed by author id 258 */ 259 private function get_author_context_ids(array $authorids) : array { 260 $authorvault = $this->vaultfactory->get_author_vault(); 261 return $authorvault->get_context_ids_for_author_ids($authorids); 262 } 263 264 /** 265 * Load the list of all attachments for the posts. The list of attachments will be 266 * indexed by the post id. 267 * 268 * @param array $groupedposts List of posts grouped by discussions. 269 * @return stored_file[] 270 */ 271 private function get_attachments_for_posts(array $groupedposts) : array { 272 $attachmentsbypostid = []; 273 $postattachmentvault = $this->vaultfactory->get_post_attachment_vault(); 274 $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { 275 ['forum' => $forum, 'posts' => $posts] = $grouping; 276 277 $forumid = $forum->get_id(); 278 if (!isset($carry[$forumid])) { 279 $carry[$forumid] = [ 280 'forum' => $forum, 281 'posts' => [] 282 ]; 283 } 284 285 $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); 286 return $carry; 287 }, []); 288 289 foreach ($postsbyforum as $grouping) { 290 ['forum' => $forum, 'posts' => $posts] = $grouping; 291 $attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts); 292 293 // Have to loop in order to maintain the correct indexes since they are numeric. 294 foreach ($attachments as $postid => $attachment) { 295 $attachmentsbypostid[$postid] = $attachment; 296 } 297 } 298 299 return $attachmentsbypostid; 300 } 301 302 /** 303 * Get the groups for each author of the given posts. 304 * 305 * The results are grouped by course and then author id because the groups are 306 * contextually related to the course, e.g. a single author can be part of two different 307 * sets of groups in two different courses. 308 * 309 * @param array $groupedposts List of posts grouped by discussions. 310 * @return array List of groups indexed by forum id and then author id. 311 */ 312 private function get_author_groups_from_posts(array $groupedposts) : array { 313 $groupsbyauthorid = []; 314 $authoridsbycourseid = []; 315 316 // Get the unique list of author ids for each course in the grouped 317 // posts. Grouping by course is the largest grouping we can achieve. 318 foreach ($groupedposts as $grouping) { 319 ['forum' => $forum, 'posts' => $posts] = $grouping; 320 $course = $forum->get_course_record(); 321 $courseid = $course->id; 322 323 if (!isset($authoridsbycourseid[$courseid])) { 324 $coursemodule = $forum->get_course_module_record(); 325 $authoridsbycourseid[$courseid] = [ 326 'groupingid' => $coursemodule->groupingid, 327 'authorids' => [] 328 ]; 329 } 330 331 $authorids = array_map(function($post) { 332 return $post->get_author_id(); 333 }, $posts); 334 335 foreach ($authorids as $authorid) { 336 $authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid; 337 } 338 } 339 340 // Load each set of groups per course. 341 foreach ($authoridsbycourseid as $courseid => $values) { 342 ['groupingid' => $groupingid, 'authorids' => $authorids] = $values; 343 $authorgroups = groups_get_all_groups( 344 $courseid, 345 array_keys($authorids), 346 $groupingid, 347 'g.*, gm.id, gm.groupid, gm.userid' 348 ); 349 350 if (!isset($groupsbyauthorid[$courseid])) { 351 $groupsbyauthorid[$courseid] = []; 352 } 353 354 foreach ($authorgroups as $group) { 355 // Clean up data returned from groups_get_all_groups. 356 $userid = $group->userid; 357 $groupid = $group->groupid; 358 359 unset($group->userid); 360 unset($group->groupid); 361 $group->id = $groupid; 362 363 if (!isset($groupsbyauthorid[$courseid][$userid])) { 364 $groupsbyauthorid[$courseid][$userid] = []; 365 } 366 367 $groupsbyauthorid[$courseid][$userid][] = $group; 368 } 369 } 370 371 return $groupsbyauthorid; 372 } 373 374 /** 375 * Get the list of tags for each of the posts. The tags will be returned in an 376 * array indexed by the post id. 377 * 378 * @param post_entity[] $posts The list of posts to load tags for. 379 * @return array Sets of tags indexed by post id. 380 */ 381 private function get_tags_from_posts(array $posts) : array { 382 $postids = array_map(function($post) { 383 return $post->get_id(); 384 }, $posts); 385 return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids); 386 } 387 388 /** 389 * Get the list of ratings for each post. The ratings are returned in an array 390 * indexed by the post id. 391 * 392 * @param stdClass $user The user viewing the ratings. 393 * @param array $groupedposts List of posts grouped by discussions. 394 * @return array Sets of ratings indexed by post id. 395 */ 396 private function get_ratings_from_posts(stdClass $user, array $groupedposts) { 397 $ratingsbypostid = []; 398 $postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper(); 399 $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { 400 ['forum' => $forum, 'posts' => $posts] = $grouping; 401 402 $forumid = $forum->get_id(); 403 if (!isset($carry[$forumid])) { 404 $carry[$forumid] = [ 405 'forum' => $forum, 406 'posts' => [] 407 ]; 408 } 409 410 $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); 411 return $carry; 412 }, []); 413 414 foreach ($postsbyforum as $grouping) { 415 ['forum' => $forum, 'posts' => $posts] = $grouping; 416 417 if (!$forum->has_rating_aggregate()) { 418 continue; 419 } 420 421 $items = $postsdatamapper->to_legacy_objects($posts); 422 $ratingoptions = (object) [ 423 'context' => $forum->get_context(), 424 'component' => 'mod_forum', 425 'ratingarea' => 'post', 426 'items' => $items, 427 'aggregate' => $forum->get_rating_aggregate(), 428 'scaleid' => $forum->get_scale(), 429 'userid' => $user->id, 430 'assesstimestart' => $forum->get_assess_time_start(), 431 'assesstimefinish' => $forum->get_assess_time_finish() 432 ]; 433 434 $rm = $this->ratingmanager; 435 $items = $rm->get_ratings($ratingoptions); 436 437 foreach ($items as $item) { 438 $ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating; 439 } 440 } 441 442 return $ratingsbypostid; 443 } 444 445 /** 446 * Get the read receipt collections for the given viewing user and each forum. The 447 * receipt collections will only be loaded for posts in forums that the user is tracking. 448 * 449 * The receipt collections are returned in an array indexed by the forum ids. 450 * 451 * @param stdClass $user The user viewing the posts. 452 * @param array $groupedposts List of posts grouped by discussions. 453 */ 454 private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) { 455 $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); 456 $trackedforums = []; 457 $trackedpostids = []; 458 459 foreach ($groupedposts as $group) { 460 ['forum' => $forum, 'posts' => $posts] = $group; 461 $forumid = $forum->get_id(); 462 463 if (!isset($trackedforums[$forumid])) { 464 $forumrecord = $forumdatamapper->to_legacy_object($forum); 465 $trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user); 466 } 467 468 if ($trackedforums[$forumid]) { 469 foreach ($posts as $post) { 470 $trackedpostids[] = $post->get_id(); 471 } 472 } 473 } 474 475 if (empty($trackedpostids)) { 476 return []; 477 } 478 479 // We can just load a single receipt collection for all tracked posts. 480 $receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault(); 481 $readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids); 482 $receiptsbyforumid = []; 483 484 // Assign the collection to all forums that are tracked. 485 foreach ($trackedforums as $forumid => $tracked) { 486 if ($tracked) { 487 $receiptsbyforumid[$forumid] = $readreceiptcollection; 488 } 489 } 490 491 return $receiptsbyforumid; 492 } 493 494 /** 495 * Sort the list of exported posts back into the same order as the given posts. 496 * The ordering of the exported posts can often deviate from the given posts due 497 * to the process of exporting them so we need to sort them back into the order 498 * that the calling code expected. 499 * 500 * @param post_entity[] $posts The posts in the expected order. 501 * @param stdClass[] $exportedposts The list of exported posts in any order. 502 * @return stdClass[] Sorted exported posts. 503 */ 504 private function sort_exported_posts(array $posts, array $exportedposts) { 505 $postindexes = []; 506 foreach (array_values($posts) as $index => $post) { 507 $postindexes[$post->get_id()] = $index; 508 } 509 510 $sortedexportedposts = []; 511 512 foreach ($exportedposts as $exportedpost) { 513 $index = $postindexes[$exportedpost->id]; 514 $sortedexportedposts[$index] = $exportedpost; 515 } 516 517 return $sortedexportedposts; 518 } 519 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body