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