See Release Notes
Long Term Support Release
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 * Renderer factory. 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\factories; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use mod_forum\grades\forum_gradeitem; 30 use mod_forum\local\entities\discussion as discussion_entity; 31 use mod_forum\local\entities\forum as forum_entity; 32 use mod_forum\local\factories\vault as vault_factory; 33 use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; 34 use mod_forum\local\factories\entity as entity_factory; 35 use mod_forum\local\factories\exporter as exporter_factory; 36 use mod_forum\local\factories\manager as manager_factory; 37 use mod_forum\local\factories\builder as builder_factory; 38 use mod_forum\local\factories\url as url_factory; 39 use mod_forum\local\renderers\discussion as discussion_renderer; 40 use mod_forum\local\renderers\discussion_list as discussion_list_renderer; 41 use mod_forum\local\renderers\posts as posts_renderer; 42 use moodle_page; 43 use core\output\notification; 44 45 /** 46 * Renderer factory. 47 * 48 * See: 49 * https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html 50 * 51 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 53 */ 54 class renderer { 55 /** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */ 56 private $legacydatamapperfactory; 57 /** @var exporter_factory $exporterfactory Exporter factory */ 58 private $exporterfactory; 59 /** @var vault_factory $vaultfactory Vault factory */ 60 private $vaultfactory; 61 /** @var manager_factory $managerfactory Manager factory */ 62 private $managerfactory; 63 /** @var entity_factory $entityfactory Entity factory */ 64 private $entityfactory; 65 /** @var builder_factory $builderfactory Builder factory */ 66 private $builderfactory; 67 /** @var url_factory $urlfactory URL factory */ 68 private $urlfactory; 69 /** @var renderer_base $rendererbase Renderer base */ 70 private $rendererbase; 71 /** @var moodle_page $page Moodle page */ 72 private $page; 73 74 /** 75 * Constructor. 76 * 77 * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory 78 * @param exporter_factory $exporterfactory Exporter factory 79 * @param vault_factory $vaultfactory Vault factory 80 * @param manager_factory $managerfactory Manager factory 81 * @param entity_factory $entityfactory Entity factory 82 * @param builder_factory $builderfactory Builder factory 83 * @param url_factory $urlfactory URL factory 84 * @param moodle_page $page Moodle page 85 */ 86 public function __construct( 87 legacy_data_mapper_factory $legacydatamapperfactory, 88 exporter_factory $exporterfactory, 89 vault_factory $vaultfactory, 90 manager_factory $managerfactory, 91 entity_factory $entityfactory, 92 builder_factory $builderfactory, 93 url_factory $urlfactory, 94 moodle_page $page 95 ) { 96 $this->legacydatamapperfactory = $legacydatamapperfactory; 97 $this->exporterfactory = $exporterfactory; 98 $this->vaultfactory = $vaultfactory; 99 $this->managerfactory = $managerfactory; 100 $this->entityfactory = $entityfactory; 101 $this->builderfactory = $builderfactory; 102 $this->urlfactory = $urlfactory; 103 $this->page = $page; 104 $this->rendererbase = $page->get_renderer('mod_forum'); 105 } 106 107 /** 108 * Create a discussion renderer for the given forum and discussion. 109 * 110 * @param forum_entity $forum Forum the discussion belongs to 111 * @param discussion_entity $discussion Discussion to render 112 * @param int $displaymode How should the posts be formatted? 113 * @return discussion_renderer 114 */ 115 public function get_discussion_renderer( 116 forum_entity $forum, 117 discussion_entity $discussion, 118 int $displaymode 119 ) : discussion_renderer { 120 121 $capabilitymanager = $this->managerfactory->get_capability_manager($forum); 122 $ratingmanager = $this->managerfactory->get_rating_manager(); 123 $rendererbase = $this->rendererbase; 124 125 $baseurl = $this->urlfactory->get_discussion_view_url_from_discussion($discussion); 126 $notifications = []; 127 128 return new discussion_renderer( 129 $forum, 130 $discussion, 131 $displaymode, 132 $rendererbase, 133 $this->get_single_discussion_posts_renderer($displaymode, false), 134 $this->page, 135 $this->legacydatamapperfactory, 136 $this->exporterfactory, 137 $this->vaultfactory, 138 $this->urlfactory, 139 $this->entityfactory, 140 $capabilitymanager, 141 $ratingmanager, 142 $this->entityfactory->get_exported_posts_sorter(), 143 $baseurl, 144 $notifications, 145 function($discussion, $user, $forum) { 146 $exportbuilder = $this->builderfactory->get_exported_discussion_builder(); 147 return $exportbuilder->build( 148 $user, 149 $forum, 150 $discussion 151 ); 152 } 153 ); 154 } 155 156 /** 157 * Create a posts renderer to render posts without defined parent/reply relationships. 158 * 159 * @return posts_renderer 160 */ 161 public function get_posts_renderer() : posts_renderer { 162 return new posts_renderer( 163 $this->rendererbase, 164 $this->builderfactory->get_exported_posts_builder(), 165 'mod_forum/forum_discussion_posts' 166 ); 167 } 168 169 /** 170 * Create a posts renderer to render a list of posts in a single discussion. 171 * 172 * @param int|null $displaymode How should the posts be formatted? 173 * @param bool $readonly Should the posts include the actions to reply, delete, etc? 174 * @return posts_renderer 175 */ 176 public function get_single_discussion_posts_renderer(int $displaymode = null, bool $readonly = false) : posts_renderer { 177 $exportedpostssorter = $this->entityfactory->get_exported_posts_sorter(); 178 179 switch ($displaymode) { 180 case FORUM_MODE_THREADED: 181 $template = 'mod_forum/forum_discussion_threaded_posts'; 182 break; 183 case FORUM_MODE_NESTED: 184 $template = 'mod_forum/forum_discussion_nested_posts'; 185 break; 186 case FORUM_MODE_NESTED_V2: 187 $template = 'mod_forum/forum_discussion_nested_v2_posts'; 188 break; 189 default; 190 $template = 'mod_forum/forum_discussion_posts'; 191 break; 192 } 193 194 return new posts_renderer( 195 $this->rendererbase, 196 $this->builderfactory->get_exported_posts_builder(), 197 $template, 198 // Post process the exported posts for our template. This function will add the "replies" 199 // and "hasreplies" properties to the exported posts. It will also sort them into the 200 // reply tree structure if the display mode requires it. 201 function($exportedposts, $forums, $discussions) use ($displaymode, $readonly, $exportedpostssorter) { 202 $forum = array_shift($forums); 203 $seenfirstunread = false; 204 $postcount = count($exportedposts); 205 $discussionsbyid = array_reduce($discussions, function($carry, $discussion) { 206 $carry[$discussion->get_id()] = $discussion; 207 return $carry; 208 }, []); 209 $exportedposts = array_map( 210 function($exportedpost) use ($forum, $discussionsbyid, $readonly, $seenfirstunread, $displaymode) { 211 $discussion = $discussionsbyid[$exportedpost->discussionid] ?? null; 212 if ($forum->get_type() == 'single' && !$exportedpost->hasparent) { 213 // Remove the author from any posts that don't have a parent. 214 unset($exportedpost->author); 215 unset($exportedpost->html['authorsubheading']); 216 } 217 218 $exportedpost->firstpost = false; 219 $exportedpost->readonly = $readonly; 220 $exportedpost->hasreplycount = false; 221 $exportedpost->hasreplies = false; 222 $exportedpost->replies = []; 223 $exportedpost->discussionlocked = $discussion ? $discussion->is_locked() : null; 224 225 $exportedpost->isfirstunread = false; 226 if (!$seenfirstunread && $exportedpost->unread) { 227 $exportedpost->isfirstunread = true; 228 $seenfirstunread = true; 229 } 230 231 if ($displaymode === FORUM_MODE_NESTED_V2) { 232 $exportedpost->showactionmenu = $exportedpost->capabilities['view'] || 233 $exportedpost->capabilities['controlreadstatus'] || 234 $exportedpost->capabilities['edit'] || 235 $exportedpost->capabilities['split'] || 236 $exportedpost->capabilities['delete'] || 237 $exportedpost->capabilities['export'] || 238 !empty($exportedpost->urls['viewparent']); 239 } 240 241 return $exportedpost; 242 }, 243 $exportedposts 244 ); 245 246 if ( 247 $displaymode === FORUM_MODE_NESTED || 248 $displaymode === FORUM_MODE_THREADED || 249 $displaymode === FORUM_MODE_NESTED_V2 250 ) { 251 $sortedposts = $exportedpostssorter->sort_into_children($exportedposts); 252 $sortintoreplies = function($nestedposts) use (&$sortintoreplies) { 253 return array_map(function($postdata) use (&$sortintoreplies) { 254 [$post, $replies] = $postdata; 255 $totalreplycount = 0; 256 257 if (empty($replies)) { 258 $post->replies = []; 259 $post->hasreplies = false; 260 } else { 261 $sortedreplies = $sortintoreplies($replies); 262 // Set the parent author name on the replies. This is used for screen 263 // readers to help them identify the structure of the discussion. 264 $sortedreplies = array_map(function($reply) use ($post) { 265 if (isset($post->author)) { 266 $reply->parentauthorname = $post->author->fullname; 267 } else { 268 // The only time the author won't be set is for a single discussion 269 // forum. See above for where it gets unset. 270 $reply->parentauthorname = get_string('firstpost', 'mod_forum'); 271 } 272 return $reply; 273 }, $sortedreplies); 274 275 $totalreplycount = array_reduce($sortedreplies, function($carry, $reply) { 276 return $carry + 1 + $reply->totalreplycount; 277 }, $totalreplycount); 278 279 $post->replies = $sortedreplies; 280 $post->hasreplies = true; 281 } 282 283 $post->totalreplycount = $totalreplycount; 284 285 return $post; 286 }, $nestedposts); 287 }; 288 // Set the "replies" property on the exported posts. 289 $exportedposts = $sortintoreplies($sortedposts); 290 } else if ($displaymode === FORUM_MODE_FLATNEWEST || $displaymode === FORUM_MODE_FLATOLDEST) { 291 $exportedfirstpost = array_shift($exportedposts); 292 $exportedfirstpost->replies = $exportedposts; 293 $exportedfirstpost->hasreplies = true; 294 $exportedposts = [$exportedfirstpost]; 295 } 296 297 if (!empty($exportedposts)) { 298 // Need to identify the first post so that we can use it in behat tests. 299 $exportedposts[0]->firstpost = true; 300 $exportedposts[0]->hasreplycount = true; 301 $exportedposts[0]->replycount = $postcount - 1; 302 } 303 304 return $exportedposts; 305 } 306 ); 307 } 308 309 /** 310 * Create a posts renderer to render posts in the forum search results. 311 * 312 * @param string[] $searchterms The search terms to be highlighted in the posts 313 * @return posts_renderer 314 */ 315 public function get_posts_search_results_renderer(array $searchterms) : posts_renderer { 316 $urlfactory = $this->urlfactory; 317 318 return new posts_renderer( 319 $this->rendererbase, 320 $this->builderfactory->get_exported_posts_builder(), 321 'mod_forum/forum_search_results', 322 // Post process the exported posts to add the highlighting of the search terms to the post 323 // and also the additional context links in the subject. 324 function($exportedposts, $forumsbyid, $discussionsbyid) use ($searchterms, $urlfactory) { 325 $highlightwords = implode(' ', $searchterms); 326 327 return array_map( 328 function($exportedpost) use ( 329 $forumsbyid, 330 $discussionsbyid, 331 $searchterms, 332 $highlightwords, 333 $urlfactory 334 ) { 335 $discussion = $discussionsbyid[$exportedpost->discussionid]; 336 $forum = $forumsbyid[$discussion->get_forum_id()]; 337 338 $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion); 339 $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false); 340 $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false); 341 $exportedpost->subject = highlight($highlightwords, $exportedpost->subject); 342 $exportedpost->forumname = format_string($forum->get_name(), true); 343 $exportedpost->discussionname = highlight($highlightwords, format_string($discussion->get_name(), true)); 344 $exportedpost->showdiscussionname = $forum->get_type() != 'single'; 345 346 // Identify search terms only found in HTML markup, and add a warning about them to 347 // the start of the message text. This logic was copied exactly as is from the previous 348 // implementation. 349 $missingterms = ''; 350 $exportedpost->message = highlight( 351 $highlightwords, 352 $exportedpost->message, 353 0, 354 '<fgw9sdpq4>', 355 '</fgw9sdpq4>' 356 ); 357 358 foreach ($searchterms as $searchterm) { 359 if ( 360 preg_match("/$searchterm/i", $exportedpost->message) && 361 !preg_match('/<fgw9sdpq4>' . $searchterm . '<\/fgw9sdpq4>/i', $exportedpost->message) 362 ) { 363 $missingterms .= " $searchterm"; 364 } 365 } 366 367 $exportedpost->message = str_replace('<fgw9sdpq4>', '<span class="highlight">', $exportedpost->message); 368 $exportedpost->message = str_replace('</fgw9sdpq4>', '</span>', $exportedpost->message); 369 370 if ($missingterms) { 371 $strmissingsearchterms = get_string('missingsearchterms', 'forum'); 372 $exportedpost->message = '<p class="highlight2">' . $strmissingsearchterms . ' ' 373 . $missingterms . '</p>' . $exportedpost->message; 374 } 375 376 return $exportedpost; 377 }, 378 $exportedposts 379 ); 380 } 381 ); 382 } 383 384 /** 385 * Create a posts renderer to render posts in mod/forum/user.php. 386 * 387 * @param bool $addlinkstocontext Should links to the course, forum, and discussion be included? 388 * @return posts_renderer 389 */ 390 public function get_user_forum_posts_report_renderer(bool $addlinkstocontext) : posts_renderer { 391 $urlfactory = $this->urlfactory; 392 393 return new posts_renderer( 394 $this->rendererbase, 395 $this->builderfactory->get_exported_posts_builder(), 396 'mod_forum/forum_posts_with_context_links', 397 function($exportedposts, $forumsbyid, $discussionsbyid) use ($urlfactory, $addlinkstocontext) { 398 399 return array_map(function($exportedpost) use ($forumsbyid, $discussionsbyid, $addlinkstocontext, $urlfactory) { 400 $discussion = $discussionsbyid[$exportedpost->discussionid]; 401 $forum = $forumsbyid[$discussion->get_forum_id()]; 402 $courserecord = $forum->get_course_record(); 403 404 if ($addlinkstocontext) { 405 $viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion); 406 $exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false); 407 $exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false); 408 $exportedpost->urls['viewcourse'] = $urlfactory->get_course_url_from_forum($forum)->out(false); 409 } 410 411 $exportedpost->forumname = format_string($forum->get_name(), true); 412 $exportedpost->discussionname = format_string($discussion->get_name(), true); 413 $exportedpost->coursename = format_string($courserecord->shortname, true); 414 $exportedpost->showdiscussionname = $forum->get_type() != 'single'; 415 416 return $exportedpost; 417 }, $exportedposts); 418 } 419 ); 420 } 421 422 /** 423 * Create a standard type discussion list renderer. 424 * 425 * @param forum_entity $forum The forum that the discussions belong to 426 * @return discussion_list_renderer 427 */ 428 public function get_discussion_list_renderer( 429 forum_entity $forum 430 ) : discussion_list_renderer { 431 432 $capabilitymanager = $this->managerfactory->get_capability_manager($forum); 433 $rendererbase = $this->rendererbase; 434 $notifications = []; 435 436 switch ($forum->get_type()) { 437 case 'news': 438 if (SITEID == $forum->get_course_id()) { 439 $template = 'mod_forum/frontpage_news_discussion_list'; 440 } else { 441 $template = 'mod_forum/news_discussion_list'; 442 } 443 break; 444 case 'qanda': 445 $template = 'mod_forum/qanda_discussion_list'; 446 break; 447 default: 448 $template = 'mod_forum/discussion_list'; 449 } 450 451 return new discussion_list_renderer( 452 $forum, 453 $rendererbase, 454 $this->legacydatamapperfactory, 455 $this->exporterfactory, 456 $this->vaultfactory, 457 $this->builderfactory, 458 $capabilitymanager, 459 $this->urlfactory, 460 forum_gradeitem::load_from_forum_entity($forum), 461 $template, 462 $notifications, 463 function($discussions, $user, $forum) { 464 465 $exporteddiscussionsummarybuilder = $this->builderfactory->get_exported_discussion_summaries_builder(); 466 return $exporteddiscussionsummarybuilder->build( 467 $user, 468 $forum, 469 $discussions 470 ); 471 } 472 ); 473 } 474 475 /** 476 * Create a discussion list renderer which shows more information about the first post. 477 * 478 * @param forum_entity $forum The forum that the discussions belong to 479 * @param string $template The template to use 480 * @return discussion_list_renderer 481 */ 482 private function get_detailed_discussion_list_renderer( 483 forum_entity $forum, 484 string $template 485 ) : discussion_list_renderer { 486 487 $capabilitymanager = $this->managerfactory->get_capability_manager($forum); 488 $rendererbase = $this->rendererbase; 489 $notifications = []; 490 491 return new discussion_list_renderer( 492 $forum, 493 $rendererbase, 494 $this->legacydatamapperfactory, 495 $this->exporterfactory, 496 $this->vaultfactory, 497 $this->builderfactory, 498 $capabilitymanager, 499 $this->urlfactory, 500 forum_gradeitem::load_from_forum_entity($forum), 501 $template, 502 $notifications, 503 function($discussions, $user, $forum) use ($capabilitymanager) { 504 $exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder(); 505 $discussionentries = []; 506 $postentries = []; 507 foreach ($discussions as $discussion) { 508 $discussionentries[] = $discussion->get_discussion(); 509 $discussionentriesids[] = $discussion->get_discussion()->get_id(); 510 $postentries[] = $discussion->get_first_post(); 511 } 512 513 $exportedposts['posts'] = $exportedpostsbuilder->build( 514 $user, 515 [$forum], 516 $discussionentries, 517 $postentries 518 ); 519 520 $postvault = $this->vaultfactory->get_post_vault(); 521 $canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user); 522 $discussionrepliescount = $postvault->get_reply_count_for_discussion_ids( 523 $user, 524 $discussionentriesids, 525 $canseeanyprivatereply 526 ); 527 $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); 528 $forumrecord = $forumdatamapper->to_legacy_object($forum); 529 if (forum_tp_is_tracked($forumrecord, $user)) { 530 $discussionunreadscount = $postvault->get_unread_count_for_discussion_ids( 531 $user, 532 $discussionentriesids, 533 $canseeanyprivatereply 534 ); 535 } else { 536 $discussionunreadscount = []; 537 } 538 539 array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount, $discussionunreadscount) { 540 $post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0; 541 $post->discussionunreadscount = $discussionunreadscount[$post->discussionid] ?? 0; 542 // TODO: Find a better solution due to language differences when defining the singular and plural form. 543 $post->isreplyplural = $post->discussionrepliescount != 1 ? true : false; 544 $post->isunreadplural = $post->discussionunreadscount != 1 ? true : false; 545 }); 546 547 $exportedposts['state']['hasdiscussions'] = $exportedposts['posts'] ? true : false; 548 549 return $exportedposts; 550 } 551 ); 552 } 553 554 /** 555 * Create a blog type discussion list renderer. 556 * 557 * @param forum_entity $forum The forum that the discussions belong to 558 * @return discussion_list_renderer 559 */ 560 public function get_blog_discussion_list_renderer( 561 forum_entity $forum 562 ) : discussion_list_renderer { 563 return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/blog_discussion_list'); 564 } 565 566 /** 567 * Create a discussion list renderer for the social course format. 568 * 569 * @param forum_entity $forum The forum that the discussions belong to 570 * @return discussion_list_renderer 571 */ 572 public function get_social_discussion_list_renderer( 573 forum_entity $forum 574 ) : discussion_list_renderer { 575 return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/social_discussion_list'); 576 } 577 578 /** 579 * Create a discussion list renderer for the social course format. 580 * 581 * @param forum_entity $forum The forum that the discussions belong to 582 * @return discussion_list_renderer 583 */ 584 public function get_frontpage_news_discussion_list_renderer( 585 forum_entity $forum 586 ) : discussion_list_renderer { 587 return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/frontpage_social_discussion_list'); 588 } 589 590 /** 591 * Create a single type discussion list renderer. 592 * 593 * @param forum_entity $forum Forum the discussion belongs to 594 * @param discussion_entity $discussion The discussion entity 595 * @param bool $hasmultiplediscussions Whether the forum has multiple discussions (more than one) 596 * @param int $displaymode How should the posts be formatted? 597 * @return discussion_renderer 598 */ 599 public function get_single_discussion_list_renderer( 600 forum_entity $forum, 601 discussion_entity $discussion, 602 bool $hasmultiplediscussions, 603 int $displaymode 604 ) : discussion_renderer { 605 606 $capabilitymanager = $this->managerfactory->get_capability_manager($forum); 607 $ratingmanager = $this->managerfactory->get_rating_manager(); 608 $rendererbase = $this->rendererbase; 609 610 $cmid = $forum->get_course_module_record()->id; 611 $baseurl = $this->urlfactory->get_forum_view_url_from_course_module_id($cmid); 612 $notifications = array(); 613 614 if ($hasmultiplediscussions) { 615 $notifications[] = (new notification(get_string('warnformorepost', 'forum'))) 616 ->set_show_closebutton(true); 617 } 618 619 return new discussion_renderer( 620 $forum, 621 $discussion, 622 $displaymode, 623 $rendererbase, 624 $this->get_single_discussion_posts_renderer($displaymode, false), 625 $this->page, 626 $this->legacydatamapperfactory, 627 $this->exporterfactory, 628 $this->vaultfactory, 629 $this->urlfactory, 630 $this->entityfactory, 631 $capabilitymanager, 632 $ratingmanager, 633 $this->entityfactory->get_exported_posts_sorter(), 634 $baseurl, 635 $notifications 636 ); 637 } 638 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body